说说Vue2的响应式原理
时间:2023-03-28 17:43:41
HTML
什么是响应式?在Vue开发中,如果我们修改了数据,所有使用到这个数据的视图都会被更新。简而言之,响应性是数据驱动视图的自动更新。例如本文也将使用如下代码来讲解和实现响应式HTML{{obj.message}}
JSletdata={obj:{message:'HelloVue!',},}newVue({el:'#app',data,})setTimeout(()=>{data.obj={message:'Objhavechanged!',}},1000)setTimeout(()=>{data.obj.message='Messagehavechanged!'},2000)Vue将渲染“HelloVue!”在页面上,一秒后修改data.obj,页面更新为“Objhavechanged!”,再过一秒,修改data.obj.message,页面显示为“Messagehave变了!”解决2个问题数据什么时候发生变化---监控数据用在什么地方---解析模板监控数据Vue使用3个类实现数据拦截,更新订阅发布Observer,监控类,监控数据变化,当数据发生变化时告诉通知者,在这个类中用Object.defineProperty重新定义数据的所有属性,绑定访问器(getter/setter)Dep,通知者类,通知订阅者更新视图,因为一个数据可能是多处使用,所以一个notifier会存储多个订阅者Watcher,订阅者类,用于存储数据变化后要执行的update函数,调用update函数可以用新数据更新视图下面详细说说Vue是如何实现的对我们传入的数据进行操作,Vue获取数据对象,创建监视器,绑定到对象的__ob__属性上。创建监视器将使用对象的所有属性作为Object。defineProperty再次定义,同时为每个数据创建通知程序。通知器将在闭包的上下文中创建,只有数据的访问者才能访问它。如果对象的值中还有对象,就会递归处理上面的数据,将其所有属性映射到Vue实例中(允许vm.xxx直接访问)。以本文为例,当监控数据完成后,创建两个monitor或者,分别是monitordata和obj。创建了四个notifier,分别属于dataobj和message以及两个monitor。为了后面解释方便,我们将解析这些通知器的模板数据。监控数据后,Vue会解析模板。第一个进程会创建一个虚拟节点vnode,匹配{{}}v-等响应式写法,会根据当前节点类型、响应式语法等为节点创建更新函数patch,并将数据绑定到看法。这篇文章主要是为了帮助理解响应式更新的逻辑,模板解析语法不是本文的重点,所以后面的讲解和实现中都会用到DOM节点。接下来,我们将继续使用本文中的例子来解释这个过程。Vue会使用el:'#app'配置项获取根DOM元素,遍历其所有子节点,发现一个文本节点的内容为{{obj.message}},检测到具体的响应式写法,建立一个update函数和提取数据表达式(string'obj.message}}')因为操作的只是文本节点的内容,所以update函数比较简单(下面的代码)constpatch=(value)=>{node.textContent=value}创建订阅者,保存更新函数和数据表达式,并将这个订阅者存储在全局变量中(表示进入依赖收集阶段),然后执行更新函数根据表达式访问数据函数,触发monitor绑定到它的getter,getter是从全局变量获取订阅者,存储在绑定到它的notifier中。在每次数据访问结束时,意味着本轮依赖收集完成,全局变量中的订阅者被清除,然后对于模板中使用的每一个响应数据,都会重复上述过程。对于多层嵌套的数据,也是转化为字符串,进行逐层访问。监控器会把本轮订阅者加入到一路上所有数据的通知者中。模板分析完成后,只创建了一个订阅者,但是添加到了四个通知者中,仔细想想就会发现,Dep2和Dep3的内容是完全一样的。其实Dep2主要是用来给vue的其他API更新数据的。修改数据后会触发monitor的setter,setter可以告诉notifier通知其他内部订阅者执行update函数修改view,实现了数据的响应式。如果修改的数据值是一个对象,它会先为它创建一个监视器,然后告诉通知者发布和订阅。当执行update函数访问数据时,所有child数据的newnotifier也保存了之前解析template时创建的subscribers。更新过程是基于一个例子来解释的。第一个修改是data.obj,是一个对象。原对象的monitor、Dep2、Dep4被移除并触发。obj的setter,新值是一个对象,为它创建一个monitor,并创建一个空的Dep2,Dep4告诉Dep3,通知里面的订阅者执行update函数。update函数执行访问obj和obj.message,并将这个订阅者添加到Dep2和Dep4,第二个修改是data.obj.message触发消息的setter,告诉Dep4通知订阅者更新视图。这是完整的更新过程。通知程序使用集合来存储订阅者。因此,多次访问只会添加一个订阅者代码。Vue的源代码非常复杂。本文仅摘录一小部分。下面的代码只实现了响应式更新文本节点的功能//publicVueclassclassVue{constructor(options){//savedatathis._data=options.data//createmonitorobserve(this._data)//mapdatatoinstancethis._initData()//模板解析compile(options.el,this)}//遍历数据映射到实例_initData(){for(constkeyofObject.keys(this._data)){Object.defineProperty(this,key,{get(){返回is._data[key]},set(newVal){this._data[key]=newVal},})}}}//创建监视器并返回函数observe(obj){//如果不是对象,不需要创建Monitorif(typeofobj!='object')returnnullletobif(typeofobj.__ob__!=='undefined'){ob=obj.__ob__}else{//创建monitor,passobjectob=newObserver(obj)}returnob}//MonitorclassObserver{constructor(obj){//创建通知器this.dep=newDep()//给对象添加监听器Object.defineProperty(obj,'__ob__',{value:this,})//遍历对象数据,定义访问器for(letkofObject.keys(obj)){defineReactive(obj,k)}}}//notifierclassDep{constructor(){//使用集合来存储自己的订阅者this.subs=newSet()}//添加订阅者addSub(watcher){//存储订阅者this.subs.add(watcher)}//发布和订阅notify(){//顺序执行更新函数//浅克隆是为了避免修改订阅者中的相同数据,infiniteupdatefor(constsubof[...this.subs]){sub.update()}}//标志,表示是否处于依赖收集阶段,值为watherstatictarget=null}//定义访问拦截器,创建闭包环境函数defineReactive(data,key){//为数据创建通知器constdep=newDep()//在闭包环境中使用局部变量保存数据letval=data[key]//如果子数据是一个对象,同时创建MonitorletchildOb=observe(val)//定义访问器Object.defineProperty(data,key,{//getterget(){//如果在依赖收集阶段if(Dep.target!=null){//添加Subscriptiondep.addSub(Dep.target)//Monitor的通知器也添加订阅if(childOb!=null){childOb.dep.addSub(Dep.target)}}//从局部变量获取值returnval},//setterset(newValue){if(val===newValue){return}//更新局部变量val=newValue//新值也需要尝试创建监视器childOb=observe(newValue)//告诉通知者发布订阅dep.notify()},})}//订阅者类Watcher{constructor(vue,expression,callback){this.target=vuethis.expression=expressionthis.callback=callbackthis.value=this.get()}update(){//获取新值,如果不相等,执行更新函数constvalue=this.get()if(value!==this.value){this.value=valuethis.callback.call(this.target,value)}}get(){//进入依赖收集阶段,让全局的Dep.target设置为Watcher本身Dep.target=this//沿pathconsistentlyletval=getObjVal(this.target,this.expression)//依赖收集结束dep.target=nullreturnval}}//根据字符串表达式获取值functiongetObjVal(obj,exp){letval=objexp=exp.split('.')exp.forEach((k)=>{val=val[k]})returnval}//编译模板函数compile(el,vue){//得到mountnodeconst$el=document.querySelector(el)//创建片段,存储dom节点letfragment=document.createDocumentFragment()//将所有dom节点放入片段中letchildwhile((child=$el.firstChild)){分段。appendChild(child)}//匹配响应式写法constreg=/\{\{(.*)\}\}///遍历子节点//为了简单起见,直接解析文本节点为(constnodeoffragment.childNodes){consttext=node.textContentif(node.nodeType==3&®.test(text)){//获取字符串表达式letname=text.match(reg)[1].trim()//获取数据赋值来自vuenode.textContent=getObjVal(vue,name)//创建订阅者,绑定表达式和更新函数newWatcher(vue,name,(value)=>{node.textContent=value})}}//上树$el.appendChild(fragment)}SummaryVue2实现响应式的三个类很绕。希望读者慎重思考,理清关系。最后再次强调,三个类的功能观察者都会在每个对象上创建,并为其所有属性添加访问器。用于操作通知器进行发布和订阅。每个数据(包括对象)都会有一个唯一的通知器。通知程序中使用一个集合来存储所有依赖此数据的订阅者。每次在模板中使用数据时,都会创建一个订阅者。存储更新函数和数据访问表达式结语如果文章中有不清楚或不准确的地方,欢迎评论和提问。如果喜欢或者觉得有帮助,希望大家能够点赞关注,为作者打气。