当前位置: 首页 > 科技观察

Vue2.x双向绑定原理及

时间:2023-03-13 02:33:49 科技观察

Vue数据双向绑定原理的实现Vue使用Object.defineProperty()方法劫持数据,使用set和get检测数据读写。https://jsrun.net/RMIKp/embed...MVVM框架主要包括两个方面,数据变化更新视图,视图变化更新数据。视图更改以更新数据。如果是input之类的label,可以使用oninput事件。数据变化更新视图可以使用Object.definProperty()的set方法检测数据变化。当数据发生变化时,会触发这个函数,然后更新视图。.在实现过程中,我们知道如何实现双向绑定。首先我们需要对数据进行劫持和监控,所以需要设置一个Observer函数来监控所有属性的变化。如果属性发生变化,需要通知订阅者观察者查看数据是否需要更新。如果有多个订阅者,则需要一个Dep来收集这些订阅者,然后在listener观察者和watcher之间进行统一管理。还需要一个指令解析器compile来扫描解析需要监控的节点和属性。因此,流程大致是这样的:实现一个监听器Observer,对所有属性进行劫持和监听,有变化通知订阅者。实现一个订阅者Watcher,当收到属性变化通知时执行相应的函数,然后更新视图,并使用Dep收集这些Watcher。实现一个解析器Compile,用于扫描解析节点的相关指令,根据初始化模板初始化相应的订阅者。显示一个ObserverObserver是一个数据监听器。核心方法是使用Object.defineProperty()递归地为所有属性添加setter和getter方法进行监控。varlibrary={book1:{name:"",},book2:"",};observe(library);library.book1.name="vueauthoritativeguide";//已经监听到的属性名,当前值is:"Vue权威指南"library.book2="Thereisnosuchbook";//属性book2已被监听,当前值为:"Thereisnosuchbook"//添加数据检测功能defineReactive(data,key,val){observe(val);//递归遍历所有子属性letdep=newDep();//创建一个新的depObject.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){if(Dep.target){//判断是否添加订阅者,只有第一次需要添加,后面就不需要了,看Watcherfunctiondep.addSub(Dep.target);//添加订阅者}returnval;},set:function(newVal){if(val==newVal)return;//如果值没有改变,returnval=newVal;console.log("属性"+key+"已经监听到,当前值为:""+newVal.toString()+""");dep.notify();//如果数据发生变化,则不ifyallsubscribers.},});}//监控对象函数observe(data)的所有属性{if(!data||typeofdata!=="object"){return;//如果不是对象则返回}Object.keys(data).forEach(function(key){defineReactive(data,key,data[key]);});}//Dep负责收集订阅者,并在属性发生变化时触发更新函数。functionDep(){this.subs={};}Dep.prototype={addSub:function(sub){this.subs.push(sub);},notify:function(){this.subs.forEach((sub)=>sub.update());},};在思路分析中,需要有一个可以容纳订阅者的消息订阅者Dep,用于收集订阅者,并在属性发生变化时执行相应的更新函数。从代码上看,在getter中加入订阅者Dep就是在初始化的时候触发Watcher。因此,需要判断是否需要订阅者。在setter中,如果有数据发生变化,会通知所有订阅者,订阅者会更新相应的函数。至此,一个比较完整的Observer就完成了,接下来我们开始设计Watcher。要实现Watcher订阅者,Watcher在初始化时需要将自己添加到订阅者Dep中。我们已经知道监听Observer是在获取Watcher操作时执行的,所以只需要在初始化Watcher时触发相应的get函数添加相应的订阅者操作??即可。那么如何触发get呢?因为我们已经设置了Object.defineProperty(),所以我们只需要获取对应的属性值就可以触发。我们只需要在初始化订阅者Watcher时将订阅者缓存在Dep.target上,添加成功后移除即可。functionWatcher(vm,exp,cb){this.cb=cb;this.vm=vm;this.exp=exp;thisthis.value=this.get();//将自己添加到订阅者的操作}Watcher.prototype={update:function(){this.run();},run:function(){varvalue=this.vm.data[this.exp];varoldVal=this.value;if(value!==oldVal){this.value=value;this.cb.call(this.vm,value,oldVal);}},get:function(){Dep.target=this;//缓存自身,判断是否添加watcher。varvalue=this.vm.data[this.exp];//在监听器中强制get函数dep.target=null;//释放自己returnvalue;},};至此简单的watcher设计完成,接下来将Observer与Watcher关联起来,实现简单的双向绑定。因为解析器Compile还没有设计好,可以先硬编码模板数据。将代码转换为ES6构造函数,预览并尝试。https://jsrun.net/8SIKp/embed...这段代码直接传入了绑定变量,因为编译器没有实现。我们只在一个节点上设置一个数据(名称)进行绑定。然后在页面上执行newMyVue,实现双向绑定。并在两秒后进行更改,您可以看到页面也发生了变化。//MyVueproxyKeys(key){varself=this;Object.defineProperty(this,key,{enumerable:false,configurable:true,get:functionproxyGetter(){returnsself.data[key];},set:functionproxySetter(newVal){self.data[key]=newVal;}});}上面代码的作用就是把this.data的key代理给this,这样我就可以方便的用this.xx得到this.data.xx了。实现Compile上面虽然实现了双向数据绑定,但是整个过程并没有解析DOM段存储,而是固定替换,所以接下来要实现一个解析器来做数据的解析绑定。解析器编译的实现步骤:解析模板指令,替换模板数据,初始化视图。将模板与相应的更新函数绑定到相应的节点上,并初始化相应的订阅者。为了解析模板,首先需要解析DOM数据,然后对DOM元素进行相应的指令处理。因此,整个DOM操作是比较频繁的。可以新建一个fragment片段,将需要解析的DOM存放在fragment片段中进行处理。functionnodeToFragment(el){varfragment=document.createDocumentFragment();varchild=el.firstChild;while(child){//将Dom元素移动到片段中fragment.appendChild(child);child=el.firstChild;}returnfragment;}接下来,需要遍历每个节点,对包含相关指令和模板语法的节点进行特殊处理。首先,处理最简单的模板语法,使用正则表达式解析“{{variable}}”形式的语法。functioncompileElement(el){varchildNodes=el.childNodes;varself=this;[].slice.call(childNodes).forEach(function(node){varreg=/\{\{(.*)\}\}/;//match{{xx}}vartext=node.textContent;if(self.isTextNode(node)&®.test(text)){//判断是否是符合这种形式的指令{{}}self.compileText(node,reg.exec(text)[1]);}if(node.childNodes&&node.childNodes.length){self.compileElement(node);//继续递归遍历子节点}});},functioncompileText(node,exp){varself=this;varinitText=this.vm[exp];updateText(node,initText);//将初始化后的数据初始化到视图中newWatcher(this.vm,exp,function(value){//生成订阅者和绑定它定义更新函数self.updateText(node,value);});},functionupdateText(node,value){node.textContent=typeofvalue=='undefined'?'':value;}获取最外层节点后,调用compileElement函数判断所有子节点。如果该节点是文本节点,匹配{{}}形式的指令,则编译并初始化相应的参数。那么就需要为当前参数生成对应的更新函数订阅者,在数据发生变化时更新对应的DOM。这样就完成了解析、初始化、编译三个过程。接下来,修改myVue以使用模板变量进行双向数据绑定。https://jsrun.net/K4IKp/embed...添加分析事件添加compile之后,一个数据的双向绑定就基本完成了,接下来就是在Compile中添加更多的指令分析和编译,比如v-model,v-on,v-bind等添加一个v-model和v-on分析:functioncompile(node){varnodenodeAttrs=node.attributes;varself=this;Array.prototype.forEach.call(nodeAttrs,function(attr){varattrattrName=attr.name;if(isDirective(attrName)){varexp=attr.value;vardir=attrName.substring(2);if(isEventDirective(dir)){//事件指令self.compileEvent(node,self.vm,exp,dir);}else{//v-model命令self.compileModel(node,self.vm,exp,dir);}node.removeAttribute(attrName);//解析后移除属性}});}//v-指令解析函数isDirective(attr){returnattr.indexOf("v-")==0;}//on:指令解析函数isEventDirective(dir){returndir.indexOf("on:")===0;}上面的编译函数是用来遍历当前dom的所有节点属性,然后判断属性是否bute是指令属性。如果是做相应的处理(事件,监听事件,还有数据,监听数据..)完整版的myVue在MyVue中添加了挂载方法,在所有操作完成时执行。classMyVue{constructor(options){varself=this;this.data=options.data;this.methods=options.methods;Object.keys(this.data).forEach(function(key){self.proxyKeys(key);});observe(this.data);newCompile(options.el,this);options.mounted.call(this);//一切处理完成后执行挂载函数}proxyKeys(key){//this.data属性这个varself=this的代理人;Object.defineProperty(this,key,{enumerable:false,configurable:true,get:functiongetter(){returnsself.data[key];},set:functionsetter(newVal){self.data[key]=newVal;},});}}然后就可以测试了。https://jsrun.net/Y4IKp/embed...总结一下流程,回头看看这张图,是不是清晰多了?可查看的代码地址:Vue2.x双向绑定原理及实现