美文网首页
vue2.0响应式原理分析

vue2.0响应式原理分析

作者: Mr无愧于心 | 来源:发表于2019-03-06 09:45 被阅读0次

    什么是MVVM

    • MVVM是是Model-View-ViewModel的缩写,Model代表数据模型,定义数据操作的业务逻辑,View代表视图层,负责将数据模型渲染到页面上,ViewModel通过双向绑定把View和Model进行同步交互,不需要手动操作DOM的一种设计思想。

    MVVM的实现过程

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

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

    3. Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是:
      1、在自身实例化时往属性订阅器(dep)里面添加自己
      2、自身必须有一个update()方法
      3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。

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

    前提须知

    1. Object.defineProperty()
    2. 观察者模式

    开始封装

    示例
    let vm=new MVVM({
      el:'#app',
      data:{
        message:{
          a:'hello',
          b:'world'
        }
      }
    }) 
    

    以上代码 接收一个对象 包括el节点和data数据

    MVVM.js
    1. 初始模板和数据
    2. 整合Observer、Compile和Watcher三者 实现数据的响应式变化
    class MVVM{
      constructor(options){
        this.$el=options.el;
        this.$data=options.data;
        if(this.$el){//如果有要编译的模板 就开始编译
          //数据劫持 就是把数据对象的所有属性,改成带set和get方法的,
          //当然 ,把需要劫持的数据传进去
          new Observe(thia.$data);
          //模板解析 
          //用数据和元素进行编译
          //当然要把模板el传进去,还传入当前实例,方便获取真实数据使用
          new Compile(this.$el,this);
        }
      }
    }
    
    observer.js 数据劫持
    1. 基于Object.defineProperty(data,key,{get(){},set(){}});
    2. 需要把data所有属性都进行监听,包括子属性(递归)
    3. 作用是当数据变化的时候触发set,可以再set中设置监听,触发重新编译,使视图更新
    class Observer{
      constructor(data){
        this.data=data;
       
        this.observe(this.data);
        
      }
      observe(data){//循环所有的属性添加get set
        if(!data || typeof data!='object'){//data不是一个对象就不要往下进行了
          return;
        }
        //要将数据一一劫持 先获取到data的key和value
        Object.keys(data).forEach((key)=>{
          this.definedReactive(data,key,data[key]);
          this.observe(data[key]);//递归调用,给所有的属性都加get set
        })
      }
      definedReactive(data,key,value){
        Object.defineProperty(data,key,{
          get(){
            return value;
            // todo。。。
          },
          set(newValue){
            value=newValue;
            //这里数据值更新   todo。。。
          }
        })
      }
    }
    
    compile编译模板
    1. 在模板中,为避免node节点重复操作 使用fragment文档碎片
    2. 筛选文本节点(处理{{}}),和元素节点(处理v-指令)
    3. 拿到指令中对应data的真实值,进行替换,换成真实数据
    4. 在把fragment插回到模板中
    class Compile{
      constructor(el,vm){
          this.el=this.isElement(el)?el:document.querySelector(el);
          this.vm=vm;
          if(this.el){
            //如果能够取到元素 我们才开始编译
            //1.把el中的节点都放到fragment中,避免大量操作dom影响性能
            this.fragment=this.node2fragment(this.el);
            //2.编译=>提取想要的元素节点 v-model 和文本节点{{}}
            thnis.compile(this.fragment);
            //3.fragent插入回模板中
            this.el.appendChild(this.fragment);
          }
          
          
      }
      //一些辅助的方法
      isElement(el){//判断元素节点
        return el.nodeType==1;
      }
      isDirective(attr){//以v-开头的属性就是指令
        return attr.startsWith('v-');
      }
      //核心的方法
      node2fragment(el){
        let fragment=document.createDocumentFragment();
        let firstChild;
        while(firstNode=el.firstChild){//把所有真实的dom节点都放到fragment中去
          fragment.appendChild(firstChild);
          //如果将文档中的节点添加到文档碎片中,就会从文档树中移除该节点,
          //也不会在浏览器中再看到该节点
        }
        return fragment;
      }
      compile(fragment){
        //遍历所有的子节点判断节点类型
        let childNodes=fragment.childNodes;
            Array.from(childNodes).forEach(node=>{
                if(this.isElementNode(node)){//元素节点
                    //如果是元素节点,深入判断他的子元素节点
                    this.complie(node);//递归判断
                    this.compileElement(node);
                    //这里需要编译元素
                    //提取元素上的v-model属性
                }else{//文本节点
                    //这里需要编译为文本
                    this.compileText(node)
                }
            })
      }
      compileElement(node){//编译元素
        let attrs=node.attributes;//取出当前节点的所有属性(类数组)
        Array.from(attrs).forEach((attr)=>{
          let attrName=attr.name;//  取到属性名
          if(this.isDirective(attrName)){//如果这个属性是一个指令
            //拿到属性值这个变量,替换真实数据
            let expr=attr.value;  
            //如果是 message.a 需要变成data.message.a
            //把指令后边的属性expr 替换成真实的data中的数据  绑定到dom上
             //node this.vm.$data expr
             let [,type]=attrName.split('-');//解构赋值v-model-->model
              CompileUtil[type](node,this.vm,expr);
          }
        })
      }
      compileText(node){//编译文本 针对{{}}
        let expr=node.textContent;//取文本中的内容
        //console.log(typeof expr,node)
        //console.log(typeof node,node)
        //对象类型不能用正则操作,所以用textContent
        let reg=/\{\{([^}]+)\}\}/;
        if(reg.text(expr)){//说明匹配到了{{}}语法
          //处理expr,
          //把{{}}就是expr 替换成真实的data中的数据  绑定到dom上
          //node this.vm.$data expr
           CompileUtil['text'](node,this.vm,expr);
        }
      }
    }
    let CompileUtil={
      getVal(vm,expr){
        expr=expr.split('.');//[message,a];
        return expr.reduce((prev,next)=>{
          return prev[next];
        },vm.$data)
      },
      getTextVal(vm,expr){
        return  expr.replace(/\{\{([^}])+\}\}/g,(...arg)=>{
          return  this.getVal(vm,arg[1])
        })
      },
      text(node,vm,expr){
        //expr是带{{}}的 ,需要先去大括号{{message.a}}{{message.b}}==>helloworld
        //vm.$data[expr]   //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
        let updateFn=this.updater['textUpdater'];
        if(updateFn){
          updateFn(node,this.getTextVal(vm,expr))
        }
      },
      model(node,vm,expr){
        //vm.$data[expr]   //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
         if(updateFn){
            updateFn(node,this.getVal(vm,expr))
         }
      },
      updater:{
        //文本更新({{}})
        textUpdater(node,value){
          node.textContent=value;
        },
       //指令属性值更新(v-model)
        modelUpdata(node,value){
          node.value=value;
        }
      }
    }
    

    以上的代码实现了真实数据替换指令中的变量,那么怎么将数据变化和视图更新联系起来,那么就用到了watcher

    watcher
    1. 观察者的目的是给需要变化的元素增减一个观察者,当数据变化的时候执行对应的方法
    2. 当数据初始化的时候,先保存老的值,此时不调用数据更新,当值发生变化的时候就触发updata重新调用真实数据替换
    3. 那么watcher要做的事,就是保存老值,和数据变化时候,触发更新
    4. 获取真实数据需要 vm实例,指令对应的变量expr和数据更新后要执行的回调
    class Watcher{
      constructor(vm,expr,cb){
        this.vm=vm;
        this.expr=expr;
        this.cb=cb;
        //先获取一下老的值
        this.value=this.getData();
      }
      //借用一下获取真实数据的函数
      getVal(vm,expr){
        expr=expr.split('.');
        return expr.reduce((prev,next)=>{
          return prev[next];
        },vm.$data);
      }
      getData(){
        let value=this.getVal(this.vm,this.expr);//拿到真实的数据
        return value;
      }
      //对外暴露的方法updata
      updata(){
        let newValue=this.getVal(this.vm,this.expr);//当updata执行的时候,获取新的真实的值
        let oldValue=this.value;//获取老的值;
        if(newValue!=oldValue){//如果执行updata的时候新的值和老的值不相等就调用回调函数
           this.cb(newValue)///调用对应watch的callback
        }
      }
    }
    
    dep----关于订阅发布
    1. 当监听data的时候,触发属性的get获取值的时候,应该把监听器watcher进行订阅,拿到老的真实的值
    2. 当数据变化,属性的set执行应该触发watcher的updata,执行updata的回调,在updata的回调中将真实数据绑定到模板上
    3. 第一次执行属性的get时,应该是数据第一次绑定到模板,不应该触发数据监听,以后的数据变化才应该进行updata,那么有了一个特殊的做法
    class Dep{
      constructor(){
            //订阅的数组
            this.subs=[];
        }
        addSub(watcher){
            this.subs.push(watcher);
        }
        notify(){
            this.subs.forEach((watcher)=>{
                watcher.update();
            })
        }
    }
    

    结合订阅发布,实现数据的响应式变化

    1.Observer
    class Observer{
        constructor(data){
            this.data=data;
            this.observe(data)
        }
        observe(data){
            //要对这个数据监听将原有的属性改成set和get的形式
            if(!data || typeof data!=='object'){
                return;
            }
            //要将数据一一劫持 先获取到data的key和value
            Object.keys(data).forEach((key)=>{
                this.defineReactive(data,key,data[key])
                this.observe(data[key])//递归劫持
            })
        }
        //定义数据劫持
        defineReactive(obj,key,value){
            let that=this;
            let dep=new Dep();//每个变化的数据 都会对应一个数组,这个数组是存放所有更新的操作
            Object.defineProperty(obj,key,{
                enumerable:true,
                configurable:true,
                get(){
                    //如果有target说明watcher进行了初始化,增加监听
                    Dep.target&&dep.addSub(Dep.target)
                    return value;
                },
                set(newValue){//当给data属性中设置值的时候 更改获取的属性的值
                    if(newValue!=value){
                        that.observe(newValue)//劫持新的newValue(如果是对象 继续劫持)
                        value=newValue;
                        dep.notify();//通知所有人  数据更新了,触发watcher的updata函数,执行真实数据的绑定
                    }
                }
            })
        }
    }
    
    1. Compile
    class Compile{
        constructor(el,vm){
            this.el=this.isElementNode(el)?el:document.querySelector(el);
            this.vm=vm;
            if(this.el){
                //如果能够取到元素 我们才开始编译
                //1.先把这些真实的DOM移入到内存中 fragment
                let fragment=this.node2fragment(this.el);
                //2.编译=>提取想要的元素节点 v-model 和文本节点{{}}
                this.complie(fragment);
                //把编译好的fragment塞回到页面中去
                this.el.appendChild(fragment);
            }
        }
    
    
    
    
    
        /*专门写一些辅助的方法*/
        isElementNode(node){//判断是不是元素节点
            return node.nodeType==1;
        }
        isDirective(attrName){
            return attrName.startsWith('v-');
        }
    
    
        /*核心的方法*/
        node2fragment(el){//需要将el中的所有节点都放到文档碎片中去
            let fragment=document.createDocumentFragment();
            let firstChild;
            while(firstChild=el.firstChild){//把所有真实的dom节点都放到fragment中去
                fragment.appendChild(firstChild);//如果将文档中的节点添加到文档碎片中,就会从文档树中移除该节点,也不会在浏览器中再看到该节点
            }
            return fragment;
        }
        complie(fragment){//提取想要的元素节点 v-model 和文本节点{{}}
            let childNodes=fragment.childNodes;
            Array.from(childNodes).forEach(node=>{
                if(this.isElementNode(node)){//元素节点
                    //如果是元素节点,深入判断他的子元素节点
                    this.complie(node);//递归判断
                    this.compileElement(node);
                    //这里需要编译元素
                    //提取元素上的v-model属性
                }else{//文本节点
                    //这里需要编译为文本
                    this.compileText(node)
                }
            })
        }
        compileElement(node){//编译元素
            //判断当前node元素属性上有没有v-的
            let attrs=node.attributes;//取出当前节点的所有属性(类数组)
            //console.log(attrs)
            Array.from(attrs).forEach((attr)=>{
                //判断属性名是不是包含v-的指令
                let attrName=attr.name;
                if(this.isDirective(attrName)){
                    //取到真实的值方法node节点中
                    let expr = attr.value;
                    //把指令后边的属性expr 替换成真实的data中的数据  绑定到dom上
                    //node this.vm.$data expr
                    let [,type]=attrName.split('-');//解构赋值
                    CompileUtil[type](node,this.vm,expr);
                }
            })
    
        }
        compileText(node){//编译文本 针对{{}}
            let expr=node.textContent;//取文本中的内容(字符串)
            //console.log(typeof expr,node)
            //console.log(typeof node,node)//对象类型不能用正则操作
            let reg=/\{\{([^}]+)\}\}/g;
            if(reg.test(expr)){//说明匹配到了{{}}语法
                //把{{}}就是expr 替换成真实的data中的数据  绑定到dom上
                //node this.vm.$data expr
                CompileUtil['text'](node,this.vm,expr);
            }
    
        }
    
    }
    CompileUtil={
        getVal(vm,expr){
            expr=expr.split('.');//[message,a];
            return expr.reduce((prev,next)=>{
                return prev[next];
            },vm.$data);
        },
        getTextVal(vm,expr){
            return expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
                return  this.getVal(vm,arg[1])
            })
        },
        setVal(vm,expr,value){
            expr=expr.split('.');//[message,a];
            return expr.reduce((prev,next,currentIndex)=>{
                if(currentIndex===expr.length-1){
                    return prev[next]=value;
                }
                return prev[next];
            },vm.$data);
        },
        text(node,vm,expr){//文本处理
            let updateFn=this.updater['textUpdater'];
            //expr是带{{}}的 ,需要先去大括号{{message.a}}{{message.b}}==>helloworld
            //vm.$data[expr]   //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
            let val=this.getTextVal(vm,expr)
            expr.replace(/\{\{([^}]+)\}\}/g,(...arg)=>{
                    new Watcher(vm,arg[1],(newValue)=>{
                        //如果数据变化了,文本节点需要重新获取依赖的属性更新文本中的内容
                        updateFn&&updateFn(node,this.getTextVal(vm,expr));
                    })
                })
            updateFn&&updateFn(node,val);
        },
        model(node,vm,expr){//输入框处理
            let updateFn=this.updater['modelUpdater'];
            //vm.$data[expr]   //vm.$data['message.a']; 取不到数据 应该是vm.$data.message.a
    
    
    
            //这里应该加一个监控,数据变化了 应该调用watch的callback(这里只是记录原始的值 watcher的updata没有执行,只有属性的set执行的时候,才会执行cb回调,重新进行真实数据绑定)
            new Watcher(vm,expr,(newValue)=>{
                //当值变化后,会调用cb将新值传递过来
                updateFn&&updateFn(node,this.getVal(vm,expr))
            })
    
    
            node.addEventListener('input',(e)=>{
                let newValue=e.target.value;
                this.setVal(vm,expr,newValue)
            })
            updateFn&&updateFn(node,this.getVal(vm,expr))
    
        },
        updater:{
            //文本更新({{}})
            textUpdater(node,value){
                node.textContent=value
            },
            //指令属性值更新(v-model)
            modelUpdater(node,value){
                node.value=value;
            }
        }
    }
    

    3.Watcher

    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('.');//[message,a];
            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)///调用对应watch的callback
            }
        }
    }
    
    4.dep
    class Dep{
        constructor(){
            //订阅的数组
            this.subs=[];
        }
        addSub(watcher){
            this.subs.push(watcher);
        }
        notify(){
            this.subs.forEach((watcher)=>{
                watcher.update();
            })
        }
    }
    
    5.MVVM
    class MVVM{
        constructor(options){
            //一上来 先把可用的东西挂载在实例上
            this.$el=options.el;
            this.$data=options.data;
    
            //如果有要编译的模板 就开始编译
            if(this.$el){
                //数据劫持 就是把数据对象的所有属性,改成带set和get方法的
                new Observer(this.$data);
                //用数据和元素进行编译
                new Compile(this.$el,this);//并且把mvvm实例传进去,方便编译使用
    
            }
        }
    }
    

    以上实现一个数据响应式变化的MVVM框架,目前拥有v-model、{{}}指令,后期可增加其他指令的功能。

    相关文章

      网友评论

          本文标题:vue2.0响应式原理分析

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