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=https://www.it610.com/article/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=https://www.it610.com/article/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=https://www.it610.com/article/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){ returnexpr.replace(/\{\{([^}])+\}\}/g,(...arg)=>{ returnthis.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=https://www.it610.com/article/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=https://www.it610.com/article/this.getData(); } //借用一下获取真实数据的函数 getVal(vm,expr){ expr=expr.split('.'); return expr.reduce((prev,next)=>{ return prev[next]; },vm.$data); } getData(){ let value=https://www.it610.com/article/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=https://www.it610.com/article/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=https://www.it610.com/article/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)=>{ returnthis.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=https://www.it610.com/article/value; } } }

3.Watcher
class Watcher{ constructor(vm,expr,cb){ this.vm=vm; this.expr=expr; this.cb=cb; //先获取一下老的值 this.value=https://www.it610.com/article/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=https://www.it610.com/article/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=https://www.it610.com/article/options.data; //如果有要编译的模板 就开始编译 if(this.$el){ //数据劫持 就是把数据对象的所有属性,改成带set和get方法的 new Observer(this.$data); //用数据和元素进行编译 new Compile(this.$el,this); //并且把mvvm实例传进去,方便编译使用} } }

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

    推荐阅读