欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

vue之mvvm原理解析

程序员文章站 2024-02-02 11:17:10
...

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的编译过程以及使用


  • 编译的流程图

vue之mvvm原理解析

  • 整体分析

vue之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页面的使用

vue之mvvm原理解析

vue类-入口文件


vue之mvvm原理解析

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

编译模板


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

vue之mvvm原理解析

节点转文档碎片

vue之mvvm原理解析

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

编译模板

//编译模板
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类
vue之mvvm原理解析

数据劫持 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中,在数据劫持处添加发布者,当数据发生改变的时候,通知订阅者。

参考文档