当前位置: 首页 > Web前端 > vue.js

Vue2.x的双向绑定原理及

时间:2023-03-31 23:17:24 vue.js

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="vue权威指南";//属性名称已被监听,当前值为:"Vue权威指南"library.book2="无此书";//属性book2已经被监听,当前值为:"Nosuchbook"//添加数据检测函数defineReactive(data,key,val){observe(val);//递归遍历所有子属性letdep=newDep();//createanewdepObject.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){if(Dep.target){//判断是否添加订阅者,只有第一次需要添加,然后就不需要了,详见Watcher函数dep.addSub(Dep.target);//添加订阅者}returnval;},set:function(newVal){if(val==newVal)return;//如果value没有变化,returnval=newVal;console.log("Property"+key+"已经监听,当前值为:""+newVal.toString()+""");dep.notify();//如果数据改变,通知所有订阅者。},});}//监控对象的所有属性functionobserve(data){if(!data||typeofdata!=="object"){return;//如果不是对象则返回}Object.keys(data).forEach(function(key){defineReactive(data,key,data[key]);});}//Dep负责收集订阅者,并触发属性更改时更新功能。函数Dep(){this.subs={};}Dep.prototype={addSub:function(sub){this.subs.push(sub);},通知: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;这个.vm=虚拟机;这个.exp=exp;this.value=this.get();//将自己添加到订阅者的操作}Watcher.prototype={update:function(){this.run();},运行:function(){varvalue=this.vm.data[this.exp];varoldVal=this.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;//释放自己返回值;},};至此,简单的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(){returnself.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片段中进行处理。函数nodeToFragment(el){varfragment=document.createDocumentFragment();varchild=el.firstChild;while(child){//将Dom元素移动到片段中fragment.appendChild(child);孩子=el.firstChild;}returnfragment;}接下来需要遍历每个节点,对包含相关指令和模板语法的节点进行特殊处理。首先,处理最简单的模板语法,使用正则表达式解析“{{variable}}”形式的语法。函数compileElement(el){varchildNodes=el.childNodes;变种自我=这个;[].slice.call(childNodes).forEach(function(node){varreg=/\{\{(.*)\}\}/;//匹配{{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];更新文本(节点,初始化文本);//将初始化的数据初始化到视图中newWatcher(this.vm,exp,function(value){//生成订阅者并绑定更新functionself.updateText(node,value);});},functionupdateText(node,value){node.textContent=typeofvalue=='undefined'?'':value;}获取最外层节点后,调用compileElement函数判断所有子节点。如果该节点是文本节点,匹配{{}}形式的指令,则编译并初始化相应的参数。那么就需要为当前参数生成对应的更新函数订阅者,在数据发生变化时更新对应的DOM。这样就完成了解析、初始化、编译三个过程。接下来,修改myVue以使用模板变量进行双向数据绑定。https://jsrun.net/K4IKp/embed...添加分析事件添加compile之后,一个数据的双向绑定就基本完成了,接下来就是在Compile中添加更多的指令分析和编译,比如v-模型、v-on、v-bind等。添加一个v-model和v-on解析:functioncompile(node){varnodeAttrs=node.attributes;变种自我=这个;Array.prototype.forEach.call(nodeAttrs,function(attr){varattrName=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:指令parsingfunctionisEventDirective(dir){returndir.indexOf("on:")===0;}上面的编译函数是用来遍历当前dom的所有节点属性,然后判断该属性是否为指令属性,如果是做相应的处理(事件,监听事件,数据,监听数据..)完整版myVue在MyVue中增加了一个mounted方法,当所有操作完成后执行。类MyVue{构造函数(选项){varself=this;this.data=options.data;this.methods=options.methods;Object.keys(this.data).forEach(function(key){self.proxyKeys(key);});观察(这个。数据);新编译(options.el,这个);options.mounted.call(this);//一切处理完毕后执行挂载的函数}proxyKeys(key){//将this.data属性代理到thisvarself=this;Object.defineProperty(this,key,{enumerable:false,configurable:true,get:functiongetter(){returnself.data[key];},set:functionsetter(newVal){self.data[key]=newVal复制代码;},});}}然后就可以测试使用了。https://jsrun.net/Y4IKp/embed...总结一下流程,回头看看这张图,是不是清晰多了?可查看代码地址:Vue2.x的双向绑定原理及实现参考Vue.js技术揭秘-Vue.js内部运行机制分析博客园-vue的双向绑定原理及实现KKB-vue源码分析vue-study