vue之mvvm原理解析

title: vue之mvvm原理解析

mvvm 原理解析

mvvm 面试论述


MVVM分为Model、View、ViewModel三者

  • Model:代表数据模型,数据和业务逻辑都在Model层中定义;
  • View:代表UI视图,负责数据的展示;
  • ViewModel:负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;

这种模式实现了ModelView的数据自动同步,也就是双向绑定,mvvm双向绑定,采用的是数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的 setter、getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

大致的过程:

  1. 实现一个指令解析器Compile,对每个元素节点的之类进行解析,根据指令模板替换数据,以及搬到相应的更新函数

  2. 实现一个数据监控器Observer,将所有数据设置成响应式,并进行监听,如有变动可以拿到最新值并通知订阅者

  3. 实现一个订阅者Watcher,作为连接Observer(数据劫持)Compile(模板)的桥梁,在对应模板数据更新处,添加监听数据的订阅者,并将其添加到订阅者容器Dep中,当属性变动时,通过Dep发布通知,执行指令绑定的相应回调函数,从而更新视图

  4. mvvm的入口函数,主要是整合调控以上的,模板编译(compile)数据劫持(Observe)订阅者(Watcher)

mvvm的编译过程以及使用


  • 编译的流程图

编译流程图

  • 整体分析

整体分析

过程分析
new MVVM()后的编译主要分为两个部分

  1. 一部分是模板的编译 Compile

    • 编译元素和文本,将插值表达式进行替换
    • 编译模板指令的标签,例如:v-model
  2. 一部分是数据劫持 Observer

    • 将所有的数据响应式处理
    • 给模板的每个编译处设置一个观察者,并将观察者存放在Dep中
    • Watcher如果数据发生改变,在ObjectdefinePropertyset函数中调用Watcher的update方法
    • Dep发布订阅,将所有需要通知变化的data添加到一个数组中

具体步骤

1、需要 observe 的数据对象进行递归遍历,包括子属性对象的属性,都加上 setter 和 getter,这样的话,给这个对象的某个值赋值,就会触发 setter,那么就能监听到了数据变化

2、 compile 解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图

3、 Watcher 订阅者是 Observer 和 Compile 之间通信的桥梁,主要做的事情是:

  • 在自身实例化时往属性订阅器(dep)里面添加自己
  • 自身必须有一个 update() 方法
  • 待属性变动 dep.notice() 通知时,能调用自身的 update() 方法,并触发 Compile 中绑定的回调,则功成身退。

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

分解vue实例


vue的使用,

let vm = new Vue({
		el:"#app",
		data:{
			school:{
				name:"beida",
				age:100
			}
		},
	})

首先使用new创建一个vue实例,传递一个对象参数,包含eldata

实现Complie编译模板


index.html页面的使用

enter description here

vue类-入口文件


enter description here

在入口之处,先处理了模板的编译(Compile),数据劫持(Observe)在后期进行使用。

编译模板


编译模板的主要的入口,分为,将节点转成文档碎片,替换模板中的常量数据

enter description here

节点转文档碎片

enter description here

将节点转换成文档碎片,然后返回,

编译模板

//编译模板
class Compiler{


    //判断一个节点是否是元素节点
    isElementNode(node){
        return node.nodeType ```= 1;
    }

    //判断一个属性是否是一个指令
    isDirective(attrName){
        return attrName.startsWith("v-");
    }

    //编译模板
    compile(node){
        //node.childNodes 包含了元素节点与文本节点
        //得到的是一个伪数组
        let childNodes = node.childNodes;
        [...childNodes].forEach(child=>{
            if(this.isElementNode(child)){  //元素节点
                //编译元素节点
                this.compileElementNode(child)
                //递归编译所有的节点
                this.compile(child)

            }else{  //文本节点
                //编译文本节点
                this.compileText(child)
            }
        })
    }

    //编译元素节点
    compileElementNode(node){
        //获取元素的属性节点(伪数组)
        let attributes = node.attributes;
        [...attributes].forEach(attr=>{
            let {name,value: expr} = attr;

            //判断是否是一个指令
            if(this.isDirective(name)){
                let [,directive] = name.split("-")
                
                //根据指令,调用对呀的指令方法
                CompilerUtil[directive](node,expr,this.vm);
            }
        })
        
    }

    //编译文本节点
    compileText(node){
        //得到所有的文本节点
        let content = node.textContent;
        //使用正则得到所有文本里面的内容
        let reg = /\{\{(.+?)\}\}/;
        if(reg.test(content)){

            //{{}} 是v-text的语法糖,所有调用text指令
            CompilerUtil['text'](node,content,this.vm)
        }
        
    }


    //将节点转成文档碎片
    node2fragme(node){
        //创建键一个文档碎片
        let fragment = document.createDocumentFragment();
        let firstChild ;
        while(firstChild = node.firstChild){
            fragment.appendChild(firstChild)
        }
        return fragment;
    }

    
}

//编译指令处理对象--处理不同的指令
CompilerUtil = {

    //获取到data中对应的数据 
    getVal(vm,expr){
        return  expr.split(".").reduce((data,current)=>{
              return data[current]
          },vm.$data)
    },

    //设置$data中的数据
    setVal(vm,expr,value){
        expr.split(".").reduce((data,current,index,arr)=>{
            if(index ```arr.length -1){
                return data[current] = value;
            }
            return data[current]
        },vm.$data)
    },

    //处理 v-model 指令的数据
    model(node,expr,vm){
        
        
        //更新模板中在data中对应的数据
        let fn = this.updater['modelUpdater']

        //当input 框的数据相互绑定
        node.addEventListener("input",(e)=>{
            let value = e.target.value;
            //当输入框数据改变时,同步更改$data中的数据
            this.setVal(vm,expr,value);
        })

        let value = this.getVal(vm,expr)
        //替换模板中的数据
        fn(node,value)
    },

    // 处理v-text指令的数据
    text(node,expr,vm){

        let fn = this.updater['textUpdater'];
        //获取到要替换的内容
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{
            return this.getVal(vm,args[1])
        })
        fn(node,content)
    },

    //更新模板中的数据
    updater:{
        //将v-model绑定的数据进行替换
        modelUpdater(node,value){
            node.value = value;
        },

        //将v-text绑定的数据进行替换
        textUpdater(node,content){
            node.textContent = content;
        }
    }
}

编译模板,主要是对模板中的一些数据常量进行替换,对于一些指令进行相关的处理,特别是指令v-model的数据的绑定。

数据劫持


数据劫持,实在模板进行编译之前进行,将data中的所有的数据都变成响应式数据,

Observer的调用 vue类
enter description here

数据劫持 observer类

//数据劫持,将数据变成响应式数据
class Observer{
    constructor(data){
        //将数据变成响应式数据
        this.observer(data)
    }

    //将数据变成响应式数据
    observer(data){
        //判断数据是否是一个对象
        if(data && typeof data ```'object'){
            for(let key in data){
                //设置响应式
                this.defindReactive(data,key,data[key])
            }
        }
    }

    //设置响应式
    defindReactive(obj,key,value){
        //如果数据是一个对象,继续递归设置
        this.observer(value);
        let dep = new Dep();    //不用的watcher存放到不同的dep中
        Object.defineProperty(obj,key,{

            //当获取数据时会调用get
            get(){
                return value;
            },

            //当设置数据时会调用set
            set: (newValue)=>{
                if(newValue != value){
                    //将新数据设置成响应式
                    this.observer(newValue);
                    value = newValue;
                }
            }
        })
        
    }
}

订阅者的Watcher的实现


订阅者watcher

//观察者
class Watcher{
    constructor(vm,expr,cb){
        this.vm = vm;
        this.expr = expr;
        this.cb = cb;   //状态改变后要进行的操作
        //获取老数据--保存一个老状态
        this.oldValue = this.get();
    }

    //获取状态的方法
    get(){
        Dep.target = this;
        //当获取旧的值得时候便已经触发响应式数据
        let value = CompilerUtil.getVal(this.vm,this.expr)
        Dep.target = null;
        return value;
    }

    //当状态发生改变的时候,观察者更新当前的状态
    update(){
        let newVal = CompilerUtil.getVal(this.vm,this.expr);
        if(this.oldValue !```newVal){
            this.cb(newVal)
        }
    }
}

存放订阅者Dep

//存储观察者的类
class Dep {
    constructor(){
        this.subs = []; //存放所有的watcher
    }
    //添加watcher 订阅
    addSub(watcher){
        this.subs.push(watcher)
    }

    //通知发布
    notify(){
        this.subs.forEach(watcher=>watcher.update())
    }
}

订阅者,连接编译模板与数据劫持

编译模板处

//编译指令处理对象--处理不同的指令
CompilerUtil = {

     // 此处省略若该代码 ......

    //处理 v-model 指令的数据
    model(node,expr,vm){
        
        //更新模板中在data中对应的数据
        let fn = this.updater['modelUpdater']

        
        //给输入框添加一个观察者,如果数据改变,通知data数据改变
        new Watcher(vm,expr,(newValue) =>{
            fn(node,newValue)
        })

        // 此处省略若该代码 ......
    },

    // 处理v-text指令的数据
    text(node,expr,vm){

        let fn = this.updater['textUpdater'];
        let content = expr.replace(/\{\{(.+?)\}\}/g,(...args)=>{

            //添加一个订阅者
            new Watcher(vm,args[1],()=>{
                fn(node,this.getVal(vm,args[1]))
            })
            return this.getVal(vm,args[1])
        })
        fn(node,content)
    },

     // 此处省略若该代码 ......
}

数据劫持处

//数据劫持,将数据变成响应式数据
class Observer{

    // 此处省略若该代码 ...

    //设置响应式
    defindReactive(obj,key,value){
        // 此处省略若该代码 ...

        Object.defineProperty(obj,key,{

            //当获取数据时会调用get
            get(){
                Dep.target && dep.subs.push(Dep.target)
                return value;
            },

            //当设置数据时会调用set
            set: (newValue)=>{
                if(newValue != value){
                    //将新数据设置成响应式
                    this.observer(newValue);
                    value = newValue;
                    //当数据发生改变时,通知观察者
                    dep.notify();
                }
            }
        }) 
    }
}

总述:订阅者是,编译模板与数据劫持之间的桥梁,模板编译之处添加订阅者,并将订阅者存储在Dep中,在数据劫持处添加发布者,当数据发生改变的时候,通知订阅者。

参考文档

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值