vue2.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响应式原理分析)