美文网首页Android开发经验谈Android开发Android技术进阶
MVVM框架学习——实现一个Vue数据双向绑定

MVVM框架学习——实现一个Vue数据双向绑定

作者: 谁动了我的代码 | 来源:发表于2022-09-26 22:12 被阅读0次

1.什么是MVVM

MVVM即modle-view-viewmole,MVVM最早由微软提出来,在前端页面中,把Model用纯JavaScript对象表示,View负责显示,两者做到了最大限度的分离。把Model和View关联起来的就是ViewModel。ViewModel负责把Model的数据同步到View显示出来,还负责把View的修改同步回Model。

2.数据的双向绑定

在vue框架中,通过控制台或者Vue Devtools修改data里的一些属性时会看到页面也会更新,而在页面修改数据时,data里的属性值也会发生改变。我们就把这种model和view同步显示称为是双向绑定。其实单向绑定原理也差不多,视图改变data更新通过事件监听就能轻松实现了,重点都在希望data改变视图也发生改变,而这就是我们下面要讲的原理。


3.vue数据双向绑定原理

3.1 Object.defineProperty()方法

首先要知道的是vue的数据绑定通过数据劫持配合发布订阅者模式实现的,那么什么是数据劫持呢?我们可以在控制台看一下它的初始化对象是什么样的:

 let vm = new Vue({
        el:"#app",
        data:{
            obj:{
                a:
            }
        },
        created() {
            console.log(this.obj)
        },
    })

可以看到属性a分别对应着一个get 和set方法,这里引申出Object.defineProperty()方法,传递三个参数,obj(要在其上定义属性的对象)、prop(要定义或修改的属性的名称)、descriptor(将被定义或修改的属性描述符)。该方法更多信息参考:参考更多用法,着重强调一下get和set这两个属性描述键值。

  • get 存取描述符的可选键值,一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
  • set 存取描述符的可选键值,一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。

平常我们在打印一个对象属性时会这样做:

var obj = {
   name:"tnagj"
}
console.log(obj.name)  //tangj

如果我们想要在输出的同时监听obj的属性值,并且输出的是tangjSir呢?这时候我们的set和get属性就起到了很好的作用

 var obj ={};
 var name = '';
 Object.defineProperty(obj,'name',{
     set:function(value){
         name = value
         console.log('我叫:' + name)
     },
     get:function(){
        console.log(name + 'Sir')
     }
 })
 obj.name = 'tangj';  //我叫tangj
 obj.name;         //tangjSir

首先我们定义了一个obj空对象以及name空属性,再用一个Object.defineProperty()方法来判断obj.name的访问情况,如果是读值则调用get函数,如果是赋值则调用set函数。在这两个函数里面我们分别对输出的内容作了更改,因此在get方法调用时打印tangjSir,在set方法调用时打印我叫tangj。

其实这就是vue数据绑定的监听原理,我们能通过这个简单实现MVVM双向绑定。

3.2 MVVM双向绑定分析

view的变化,比如input值改变我们很容易就能知道通过input事件反应到data中,数据绑定的关键在于怎样让data更新view。首先我们要知道数据什么时候变的,上文提过可以用Object.defineProperty()的set属性描述键值来监听这个变化,当数据改变时就调用set方法。

那么我们可以设置一个监听器Observe,用来监听所有的属性,当属性变化的时候就需要告诉订阅者Watcher看是否需要更新。因为订阅者是有很多个,所以我们需要有一个消息订阅器Dep来专门收集这些订阅者,然后在监听器Observer和订阅者Watcher之间进行统一管理的。当然我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令对应初始化成一个订阅者Watcher,并替换模板数据或者绑定相应的函数,此时当订阅者Watcher接收到相应属性的变化,就会执行对应的更新函数,从而更新视图。所以,我们大致可以把整个过程拆分成五个部分,MVVM.html,MVVM.js,compile.js,observer.js,watcher.js,我们在MVVM.js中创建所需要的实例,在.html文件中引入这些js文件,这样拆分更容易理解也更好维护。

4.分步拆分

4.1 MVVM.JS

为了和Vue保持一致,我们向MVVM.js传入一个空对象options,并让vm.el = options.el,vm.data = options.data,如果能取到vm.$el再进行编译和监听

class MVVM {
       constructor(options){
       this.$el = options.el, //把东西挂载在实例上
       this.$data = options.data
        if(this.$el){    // 如果有要编译的就开始编译
            new Observer(this.$data); //数据劫持,就是把对象的所有属性改成get和set方法
            new Compile(this.$el,this);//用数据和元素进行编译
        }
   }
}

4.2 Compile.js

编译的时候有一个问题需要注意,如果直接操作DOM元素会特别消耗性能,所以我们希望先把DOM元素都放在内存中即文档碎片,待编译完成再把文档碎片放进真实的元素中

class Complie{
   constructor(el,vm){
       this.el = this.isElementNode(el)?el:document.querySelector(el);
       this.vm = vm;
       if(this.el){//如果这个元素能获取到,我们才开始编译
           let fragment = this.nodeToFragment(this.el); //1.先把真实的DOM移入到内存中,fragment
           this.compile(fragment); //2.编译=>提取想要的元素节点v-modle 和文本节点{{}}
           this.el.appendChild(fragment) //3.把编译好的fragment塞回页面
       }
       nodeToFragment(el){   //需要el元素放到内存中
       let fragment = document.createDocumentFragment();
       let Child;
       while(Child = el.firstChild){
           fragment.appendChild(Child);
       }
       return fragment;
    }
  }}

接下来我们要判断需要编译的是元素节点还是文档节点,还记得Vue中有很多很有用的指令吗?比如"v-modle"、"v-for"等,所以我们还要判断元素节点内是否包含指令,如果是指令,它应该包含一些特殊的方法

/* 省略.... */
    isElementNode(node){  //是不是元素节点
        return node.nodeType === 1;
    }
    isDirective(name){   //是不是指令
        return name.includes('v-')
    }
 compileElement(node){
          //带v-modle
          let attrs = node.attributes;
          Array.from(attrs).forEach(
              attr =>{
                 let attrName = attr.name;
                 if(this.isDirective(attrName)){
                   // 取到对应的值放到节点中
                   let expr = attr.value;
                   // node vm.$data expr
                   let [,type] = attrName.split('-')  //解构赋值
                   CompileUtil[type](node,this.vm,expr)
                 }
              }
          )
    }
    compileText(node){
         // 带{{}}
         let expr = node.textContent; //取文本的内容
         let reg = /\{\{([^}]+)\}\}/g  //全局匹配
         if(reg.test(expr)){
             // node this.vm.$data expr
             CompileUtil['text'](node,this.vm,expr)
         }
    }
    compile(fragment){  //需要递归,拿到的childNodes只是第一层
        let childNodes = fragment.childNodes;
        Array.from(childNodes).forEach(
            node=>{
                if(this.isElementNode(node)){  //是元素节点,还需要递归检查
                    this.compileElement(node)  //编译元素
                    this.compile(node)  //箭头函数this指向上一层的实例
                }else{             //文本节点
                    this.compileText(node)  //编译文本
                }
            }
        )
    }

根据获取的节点类型不同,执行不同的方法,我们可把这些方法统一都放到一个对象里面去

CompileUtil = {
    getVal(vm,expr){  //获取实例上的数据
        expr = expr.split('.'); //如果遇到vm.$data[a.a],希望先拿到vm.$data[a]
        return  expr.reduce((prev,next)=>{
            return prev[next]
        },vm.$data)
    },
    getTextVal(vm,expr){  //获取编译文本以后的结果
            return expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
                return this.getVal(vm,arguments[1]);
        })
    },
    text(node,vm,expr){       // 文本处理
       let updateFn = this.updater['textUpdater'];
       let value = this.getTextVal(vm,expr);
       updateFn && updateFn(node,value);
    },
    modle(node,vm,expr){   // 输入框处理
        let updateFn = this.updater['modleUpdater']
        updateFn && updateFn(node,this.getVal(vm,expr))
    },
    updater:{
        textUpdater(ndoe,value){
             ndoe.textContent = value //文本更新
        },
        modleUpdater(node,value){
             node.value = value
        }
    }
}

4.3 Oberver.js

编译的时候我们还需要一个监听者,当数据变化调用get和set方法

class Observer{
    constructor(data){
        this.observer(data)
    }
    observer(data){
        if(!data || typeof data !== 'object') return;
         Object.keys(data).forEach(key =>{
             this.defineReactive(data,key,data[key]);
             this.observer(data[key])
         })
    }
    defineReactive(obj,key,value){
        let that = this;
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
               Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newvalue){
                if(value === newvalue) return;
                that.observer(newvalue);  //如果新值是对象,继续劫持
                value = newvalue;
            },
        })
    }
}

4.4 Watcher订阅者和Dep监听器

前面已经实现了监听和编译,但是怎么样才能让它们之间进行通信呢,也就是当监听到变化了怎么通知呢?这里就用到了发布订阅模式。默认观察者watcher有一个update方法,它会更新数据。Dep里面创建一个数组,把观察者都放在这个数组里面,当监听到变化,一个个调用监听者update方法。

// Watcher.js
//观察者的目的就是给需要变化的那个元素增加一个观察者,当数据变化后执行对应的方法
class Watcher{
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;
        //先获取老的值
        this.value = this.get()
    }
    getVal(vm,expr){  //获取实例上的数据
        expr = expr.split('.'); //如果遇到vm.$data[a.a],希望先拿到vm.$data[a]
        // console.log(expr)
        return  expr.reduce((prev,next)=>{
            return prev[next]
        },vm.$data)
    }
    get(){
        Dep.target = this;  //缓存自己
        let value = this.getVal(this.vm,this.expr);
        Dep.target = null; //释放自己
        return value;
    }
    update(){
        let newValue = this.getVal(this.vm,this.expr);
        let oldValue = this.value;
        if(newValue != oldValue){
            this.cb(newValue);
        }
    }
}
//Dep.js
class Dep{
    constructor(){
        //订阅的数组
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(watcher =>{
            watcher.update()
        })
    }
}

watcher逻辑: 当创建watcher实例的时候,先拿到这个值,数据变化又拿到一个新值,如果新值和老值不一样,那么调用callback,实现更新;

dep逻辑:创建数组把观察者放在这个数组里,当监听到变化,执行watcher.update()

我们再它们分别添加到Observer和compile中

// complie.js
// 省略....
text(node,vm,expr){       // 文本处理
       let updateFn = this.updater['textUpdater'];
       //{{message.a}} => tangj
       let value = this.getTextVal(vm,expr);
       expr.replace(/\{\{([^}]+)\}\}/g,(...arguments)=>{
           new Watcher(vm,arguments[1],(newVaule)=>{
               // 如果数据变化,文本节点需要重新获取依赖的属性更新文本的的内容
               updateFn && updateFn(node,this.getTextVal(vm,expr));
           })
    })
       updateFn && updateFn(node,value);
    },
    modle(node,vm,expr){   // 输入框处理
        let updateFn = this.updater['modleUpdater']
         // 'message.a' => [message.a] vm.$data['message'].a
         // 这里应该加一个监控,数据变化,调用这个watch的cb
         new Watcher(vm,expr,(newVaule)=>{
             //当值变化后将调用cb,将新的值传递过来
            updateFn && updateFn(node,this.getVal(vm,expr))
         });
         node.addEventListener('input',(e)=>{
             let newVaule = e.target.value;
             this.setVal(vm,expr,newVaule)
         })
        updateFn && updateFn(node,this.getVal(vm,expr))
    }
// 省略...
// observer.jsclass Observer{
    constructor(data){
        this.observer(data)
    }
    observer(data){
        //要对这个data数据原有属性改成set和get的形式
        if(!data || typeof data !== 'object') return;
         Object.keys(data).forEach(key =>{
             this.defineReactive(data,key,data[key]);
             this.observer(data[key])
         })
    }
    defineReactive(obj,key,value){
        let that = this;
        let dep = new Dep();  //每个变化的数据都会对应一个数组,这个数据存放了所有数据的更新
        Object.defineProperty(obj,key,{
            enumerable:true,
            configurable:true,
            get(){
               Dep.target && dep.addSub(Dep.target);
                return value;
            },
            set(newvalue){
                if(value === newvalue) return;
                that.observer(newvalue);  //如果新值是对象,继续劫持
                value = newvalue;
                dep.notify(); //通知所有人数据更新
            },
        })
    }
}
class Dep{
    constructor(){
        //订阅的数组
        this.subs = []
    }
    addSub(watcher){
        this.subs.push(watcher)
    }
    notify(){
        this.subs.forEach(watcher =>{
            watcher.update()
        })
    }
}

到这里我们就实现了数据的双向绑定,MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。

当然我们还需要数据代理,用vm代理vm.$data,也是通过Object.defineProperty()实现

this.proxyData(this.$data);

proxyData(data){
       Object.keys(data).forEach(key =>{
           let val = data[key]
           Object.defineProperty(this,key,{
               enumerable:true,
               configurable:true,
               get(){
                   return val
               },
               set(newval){
                   if(val == newval){
                    return;
                   }
                   val = newval
               }
           })
       })
   }

转载自:https://www.lmlphp.com/user/57801/article/item/1578259/

学习更多架构思想可以点击这里领取↓↓↓,Android框架学习还有很多;本文只浅析了一下MVVM框架其中一个小技术点。还有MVC、MVP等框架学习需要掌握。

文末

MVVM的目标和思想与MVP类似,利用数据绑定(Data Binding)、依赖属性(Dependency Property)、命令(Command)、路由事件(Routed Event)等新特性,打造了一个更加灵活高效的架构。

MVVM(Model-View-ViewModel)框架的由来便是MVP(Model-View-Presenter)模式与WPF结合的应用方式时发展演变过来的一种新型架构框架。它立足于原有MVP框架并且把WPF的新特性糅合进去,以应对客户日益复杂的需求变化。

相关文章

  • 双向数据绑定

    双向数据绑定 双向数据绑定基于MVVM框架,vue属于MVVM框架 MVVM:M等于model,V等于view,即...

  • Vue2.0原理与MVVM的实现

    剖析Vue原理&实现双向绑定MVVM vue源码 双向绑定 -- MVVM 目前几种主流的MVC框架都实现了单向数...

  • vue双向数据绑定

    剖析Vue原理、实现双向绑定MVVM 几种实现双向绑定的做法 目前几种主流的mvc(vm)框架都实现了单向数据绑定...

  • Vue框架基础

    原生js与Vue框架的区别 用原生实现双向数据绑定 用Vue实现双向数据绑定 Vue是一个javaScript框架...

  • 搭建vue+webpack+es6环境

    1、vue介绍 vue属于VM*框架。是 MVVM框架。什么是MVVM框架?就是视图和数据的双向绑定。 vue的核...

  • vue源码——浅析响应式原理

    又到了学习源码的时间⌚️。 我们都知道vue是一个mvvm框架,数据与视图双向绑定,所有入门vue的同学,实现的第...

  • Vue原理研究之双向数据绑定

    前言 本篇文章主要研究Vue的双向数据绑定的学习笔记。具体细节请参考《剖析Vue原理&实现双向绑定MVVM》。 原...

  • Vue MVVM 原理实现

    核心原理 MVVM 双向数据绑定, 数据驱动视图 Vue 实现 MVVM 采用 数据劫持 + 发布订阅模式 : ...

  • Vue双向数据绑定原理

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

  • 关于双向绑定的问题

    剖析Vue实现原理 - 如何实现双向绑定mvvm 本文能帮你做什么?1、了解vue的双向数据绑定原理以及核心代码模...

网友评论

    本文标题:MVVM框架学习——实现一个Vue数据双向绑定

    本文链接:https://www.haomeiwen.com/subject/rwbxartx.html