vue2.0 双向绑定原理分析及简单实现

  • vue2.0 双向绑定原理分析及简单实现已关闭评论
  • 151 次浏览
  • A+
所属分类:Web前端
摘要

Vue用了有一段时间了,每当有人问到Vue双向绑定是怎么回事的时候,总是不能给大家解释的很清楚,正好最近有时间把它梳理一下,让自己理解的更清楚,下次有人问我的时候,可以侃侃而谈?。

Vue用了有一段时间了,每当有人问到Vue双向绑定是怎么回事的时候,总是不能给大家解释的很清楚,正好最近有时间把它梳理一下,让自己理解的更清楚,下次有人问我的时候,可以侃侃而谈?。

一、首先介绍Object.defineProperty()方法

//直接在一个对象上定义一个新属性,或者修改一个已经存在的属性, 并返回这个对象 Object.defineProperty(obj,prop,descriptor) 

参数

  • obj 需要定义属性的对象。
  • prop 需被定义或修改的属性名。
  • descriptor 需被定义或修改的属性的描述符。

1.1 属性描述符默认值

属性 默认值 说明
configurable false 描述属性是否可以被删除,默认为 false
enumerable false 描述属性是否可以被for...in或Object.keys枚举,默认为 false
writable false 描述属性是否可以修改,默认为 false
get undefined 当访问属性时触发该方法,默认为undefined
set undefined 当属性被修改时触发该方法,默认为undefined
value undefined 属性值,默认为undefined
// Object.defineProperty(对象,属性,属性描述符)     var obj={}     console.log('obj:',obj);      Object.defineProperty(obj, 'name', {         value: 'James'     });      console.log('obj的默认值:',obj);     delete obj.name;     console.log('obj删除后:', obj);     console.log('obj枚举:', Object.keys(obj));     obj.name = '库里';     console.log('obj修改后:', obj);     Object.defineProperty(obj, 'name', {value: '库里'}); 

运行结果:

vue2.0 双向绑定原理分析及简单实现

从运行结果可以发现,使用Object.defineProperty()定义的属性,默认是不可以被修改,不可以被枚举,不可以被删除的。可以与常规的方式定义属性对比一下:如果不使用Object.defineProperty()定义的属性,默认是可以修改、枚举、删除的:

 const obj = {};  obj.name = 'James';  console.log('枚举:', Object.keys(obj));  obj.name = ' 库里';  console.log('修改:', obj);  delete obj.name;  console.log('删除:', obj); 

运行结果:

vue2.0 双向绑定原理分析及简单实现

1.2 修改属性描述符

const o = {};   Object.defineProperty(o, 'name', {     value: 'James',        // name属性值     writable: true,       // 可以被修改     enumerable: true,     // 可以被枚举     configurable: true,   // 可以被删除   });   console.log(o);   console.log('枚举:', Object.keys(o));   o.name = '科比';   console.log('修改:', o);   Object.defineProperty(o, 'name', {     value: 'Po'   });   console.log('修改:', o);   delete o.name;   console.log('删除:', o); 

运行结果:

vue2.0 双向绑定原理分析及简单实现

结果表明,修改writable、enumerable、configurable这三个描述符为true时,属性可以被修改、枚举和删除。

注意:

1、如果writable为false,configurable为true时,通过o.name = "科比"是无法修改成功的,但是使用Object.defineProperty()修改是可以成功的

2、如果writable和configurable都为false时,如果使用Object.defineProperty()修改属性值会报错:Cannot redefine property: name

1.3 enumerable

const o = {}; Object.defineProperty(o, 'name', { value: 'James', enumerable: true }); Object.defineProperty(o, 'contact', { value: (str) => { return str+' baby' }, enumerable: false }); Object.defineProperty(o, 'age', { value: '18' }); o.skill = '前端'; console.log('枚举:', Object.keys(o)); console.log('trim: ', o.contact('nihao')) console.log(`o.propertyIsEnumerable('name'): `, o.propertyIsEnumerable('name')); console.log(`o.propertyIsEnumerable('contact'): `, o.propertyIsEnumerable('contact')); console.log(`o.propertyIsEnumerable('age'): `, o.propertyIsEnumerable('age')); 

运行结果:

vue2.0 双向绑定原理分析及简单实现

1.4 get和set

注:设置set或者get,就不能在设置value和wriable,否则会报错

const o = {     __email: ''   };   Object.defineProperty(o, 'email', {     enumerable: true,     configurable: true,     // writable: true,    // 如果设置了get或者set,writable和value属性必须注释掉     // value: '',         // writable和value无法与set和get共存     get: function () {    // 如果设置了get 或者 set 就不能设置writable和value       console.log('get', this);       return 'My email is ' + this.__email;     },     set: function (newVal) {       console.log('set', newVal);       this.__email = newVal;     }   });   console.log(o);   o.email = 'laowang@163.com';   o.email;   console.log(o);   o.email = 'laozhang@163.com';   console.log(o); 

运行结果:

vue2.0 双向绑定原理分析及简单实现

二、原理分析

2.1 最简单的双向绑定

<!DOCTYPE html> <head>     <title>最简单的双向绑定</title> </head> <body>     <div>         <input type="text" name="name" id="name" />     </div> </body> <script>     var data={         __name:''     };      Object.defineProperty(data,'name',{         enumerable: true,         configurable: true,         // writable: true,    // 如果设置了get或者set,writable和value属性必须注释掉         // value: '',         // writable和value无法与set和get共存         get: function () {    // 如果设置了get 或者 set 就不能设置writable和value             return this.__name;         },         set: function (newVal) {             this.__name=newVal;                                //更新属性             document.querySelector('#name').value = newVal;    //更新视图         }     }); 		   	//监听input事件,更新name     document.querySelector('#name').addEventListener("input",(event)=>{         data.name=event.currentTarget.value     })  </script> </html> 

运行结果:

vue2.0 双向绑定原理分析及简单实现

文本框输入"老王",查看name属性变为"老王";修改name属性为"老张",文本框变为“老张”;

最简单的双向绑定完成了?

2.2 Vue双向绑定

vue.js 则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。读完这句话是不是还有50%的懵逼,接下来继续分析。

双向绑定的经典示例图,各位细品:

vue2.0 双向绑定原理分析及简单实现

分析每个模块的作用:

Observer:数据监听器,对每个vue的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新

Compile:指令解析器,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数

Watcher:作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图

Dep:依赖收集,每个属性都有一个依赖收集对象,存储订阅该属性的Watcher

Updater:更新视图

结合原理,自定义实现Vue的双向绑定

1、首先创建index.html

<!DOCTYPE html> <html lang="en"> <head>   <meta charset="UTF-8">   <title>2.0双向绑定原理</title>   <script src="./Dep.js"></script>   <script src="./MYVM.js"></script>   <script src="./Observer.js"></script>   <script src="./Watcher.js"></script>   <script src="./TemplateCompiler.js"></script> </head>  <body>   <div id="app">     <!--模拟vue指令绑定name属性 -->     <span v-text="name"></span>     <!--模拟vue指令v-model双向绑定 -->     <input type="text" v-model="name">     <!-- 模拟{{}} -->     {{name}}   </div>   <script>     //假设已经有MYVM对象,实例化该对象     //params是一个对象 el是要挂载的dom  data是一个对象包含响应式属性     var vm = new MYVM({       el: '#app',       data: {         name: 'James'       }     })   </script> </body> </html> 

2、创建MYVM.js,主要作用是调用Observer进行数据劫持和调用TemplateCompiler进行模板解析

function MYVM(options){      //属性初始化      this.$vm=this;      this.$el=options.el;      this.$data=options.data;            //视图必须存在      if(this.$el){         //添加属性观察对象(实现数据挟持)         new Observer(this.$data)         //创建模板编译器,来解析视图         this.$compiler = new TemplateCompiler(this.$el, this.$vm)     }  } 

3、创建Observer.js,实现数据劫持

//数据解析,完成对数据属性的劫持 function Observer(data){     //判断data是否有效且data必须是对象     if(!data || typeof data !=='object' ){         return     }else{         var keys=Object.keys(data)         keys.forEach((key)=>{             this.defineReactive(data,key,data[key])         })     } } Observer.prototype.defineReactive=function(obj,key,val){     Object.defineProperty(obj,key,{         //是否可遍历         enumerable: true,         //是否可删除         configurable: false,          //取值         get(){             return val         },         //修改值         set(newVal){             val=newVal         }     }) } 

上面代码完成了数据属性的劫持,读取和修改属性会执行get、set,运行结果:

vue2.0 双向绑定原理分析及简单实现

4、给Observer.js增加订阅和发布功能,新建Dep.js,进行订阅和发布管理

//创建订阅发布者 //1.管理订阅 //2.集体通知 function Dep(){     this.subs=[]; }  //添加订阅 //参数sub是watcher对象 Dep.prototype.addSub=(sub)=>{     this.subs.push(sub) }  //集体通知,更新视图 Dep.prototype.notify=()=>{     this.subs.forEach((sub) => {         sub.update()       }) } 

5、把Dep安装到Observer.js,代码如下

//数据解析,完成对数据属性的劫持 function Observer(data){     //判断data是否有效且data必须是对象     if(!data || typeof data !=='object' ){         return     }else{         var keys=Object.keys(data)         keys.forEach((key)=>{             this.defineReactive(data,key,data[key])         })     } } Observer.prototype.defineReactive=function(obj,key,val){     //创建Dep实例     var dep=new Dep();     Object.defineProperty(obj,key,{         //是否可遍历         enumerable: true,         //是否可删除         configurable: false,          //取值         get(){             //watcher创建时,完成订阅             //检查target是否有watcher,有的话进行订阅             var watcher = Dep.target;             watcher && dep.addSub(watcher)             return val         },         //修改值         set(newVal){             val=newVal             dep.notify()         }     }) } 

var dep=new Dep() 创建了Dep的实例

get的时候检查是否有watcher,有就添加到订阅数组

set的时候通知所有的订阅者,进行视图更新

至此属性数据劫持,订阅和发布就已经实现完了

6、接下来实现模板编译器,首先创建TemplateCompiler.js

// 创建模板编译工具 // el 要编译的dom节点 // vm MYVM的当前实例 function TemplateCompiler(el,vm){     this.el = this.isElementNode(el) ? el : document.querySelector(el);     this.vm = vm;     if (this.el) {         //将对应范围的html放入内存fragment         var fragment = this.node2Fragment(this.el)         //编译模板         this.compile(fragment)         //将数据放回页面         this.el.appendChild(fragment)       } }  //是否是元素节点 TemplateCompiler.prototype.isElementNode=function(node){     return node.nodeType===1 }  //是否是文本节点 TemplateCompiler.prototype.isTextNode=function(node){     return node.nodeType===3 }  //转成数组 TemplateCompiler.prototype.toArray=function(arr){     return [].slice.call(arr) }  //判断是否是指令属性 TemplateCompiler.prototype.isDirective=function(directiveName){     return directiveName.indexOf('v-') >= 0; }  //读取dom到内存 TemplateCompiler.prototype.node2Fragment=function(node){     var fragment=document.createDocumentFragment();     var child;     //while(child=node.firstChild)这行代码,每次运行会把firstChild从node中取出,指导取出来是null就终止循环     while(child=node.firstChild){         fragment.appendChild(child)     }     return fragment; }  //编译模板 TemplateCompiler.prototype.compile=function(fragment){     var childNodes = fragment.childNodes;     var arr = this.toArray(childNodes);     arr.forEach(node => {         //判断是否是元素节点         if(this.isElementNode(node)){             this.compileElement(node);         }else{             //定义文本表达式验证规则             var textReg = /{{(.+)}}/;             var expr = node.textContent;             if (textReg.test(expr)) {                 //获取绑定的属性                 expr = RegExp.$1;                 //调用方法编译                 this.compileText(node, expr)             }         }     }); }  //解析元素节点 TemplateCompiler.prototype.compileElement=function(node){     //获取节点所有属性     var arrs=node.attributes;     this.toArray(arrs).forEach(attr => {         //获取属性名称         var attrName=attr.name;         if(this.isDirective(attrName)){             //获取v-modal的modal             var type = attrName.split('-')[1]             //获取属性对应的值(绑定的属性)             var expr = attr.value;             CompilerUtils[type] && CompilerUtils[type](node, this.vm, expr)         }       }); }   //解析文本节点  TemplateCompiler.prototype.compileText=function(node,expr){     CompilerUtils.text(node, this.vm, expr) } 

TemplateCompiler的主要逻辑:

a、dom节点读入到内存

b、遍历所有节点,判断节点类型,元素节点和文本节点分别使用不同方法编译

c、元素节点编译,遍历所有属性,根据指令名称称找到CompilerUtils对应的指令处理方法,执行视图初始化和订阅

d、文本节点编译,正则匹配找到绑定的属性,使用CompilerUtils的text执行初始化和订阅

7、创建CompilerUtils编辑工具对象,实现视图初始化和订阅

//编译工具 CompilerUtils = {   	//对应视图v-modal指令,使用该方法进行视图初始化和订阅     //params node当前节点  vm myvm对象   expr绑定的属性     //modal方法执行一次,进行视图初始化、事件订阅,添加视图到模型的事件     model(node, vm, expr) {       	//节点更新方法         var updateFn = this.updater.modelUpdater;         //初始化,更新node的值         updateFn && updateFn(node, vm.$data[expr])          //实例化一个订阅者,添加到订阅数组         new Watcher(vm, expr, (newValue) => {              //发布的时候,按照之前的规则,对节点进行更新              updateFn && updateFn(node, newValue)         })          //视图到模型(观察者模式)         node.addEventListener('input', (e) => {             //获取新值放到模型             var newValue = e.target.value;             vm.$data[expr] = newValue;         })     },      //对应视图v-text指令,使用该方法进行视图初始化和订阅     //params node当前节点  vm myvm对象   expr绑定的属性     //text方法执行一次,进行视图初始化、事件订阅     text(node, vm, expr) {         //text更新方法         var updateFn = this.updater.textUpdater;        //初始化,更新text的值         updateFn && updateFn(node, vm.$data[expr])          //实例化一个订阅者,添加到订阅数组         new Watcher(vm, expr, (newValue) => {           //发布的时候,按照之前的规则,对文本节点进行更新           updateFn && updateFn(node, newValue)         })     },      updater: {         //v-text数据更新         textUpdater(node, value) {           node.textContent = value;         },         //v-model数据更新         modelUpdater(node, value) {           node.value = value;         }     } } 

CompilerUtils的主要逻辑:

a、根据指令对节点进行数据初始化,实例化观察者Watcher到订阅数组

b、不同的指令进行不同的逻辑处理

8、创建Watcher.js,实现订阅者逻辑

//声明一个订阅者 //vm 全局vm对象 //expr 属性名称 //cb 发布时需要执行的方法 function Watcher(vm, expr, cb) {     //缓存重要属性     this.vm = vm;     this.expr = expr;     this.cb = cb;      //缓存当前值,为更新时做对比     this.value = this.get()   }   Watcher.prototype.get=function(){     //设置全局Dep的target为当前订阅者     Dep.target = this;     //获取属性的当前值,获取时会执行属性的get方法,get方法会判断target是否为空,不为空就添加订阅者     var value = this.vm.$data[this.expr]     //清空全局     Dep.target = null;     return value;   }   Watcher.prototype.update=function(){     //获取新值     var newValue = this.vm.$data[this.expr]     //获取老值     var old = this.value;      //判断后     if (newValue !== old) {       //执行回调       this.cb(newValue)     }   } 

Watcher的主要逻辑:

a、get 把当前订阅者添加到属性对应的依赖数组,保存值

b、update 发布的时候执行,进行新老值对比,更新节点内容

到此一个简单的MVVM框架就完成了,整体运行效果如下:

vue2.0 双向绑定原理分析及简单实现

梳理过程中参考很多大佬文章,感谢各位。看完基本能把VUE2.0的双向绑定原理讲清楚了,希望能帮助有缘人,?!