Vue 依赖收集原理分析
此文已由作者吴维伟授权网易云社区发布。 欢迎访问网易云社区,了解更多网易技术产品运营经验。 Vue实例在初始化时,可以接受以下几类数据:
Vue 根据实例化时接受的数据,在将数据和模板转化成DOM节点的同时,分析其依赖的数据。在特定数据改变时,自动在下一个周期重新渲染DOM节点 本文主要分析Vue是如何进行依赖收集的。 Vue中,与依赖收集相关的类有:Dep : 一个订阅者的列表类,可以增加或删除订阅者,可以向订阅者发送消息 Watcher : 订阅者类。它在初始化时可以接受getter,callback两个函数作为参数。getter用来计算Watcher对象的值。当Watcher被触发时,会重新通过getter计算当前Watcher的值,如果值改变,则会执行callback. 对初始化数据的处理对于一个Vue组件,需要一个初始化数据的生成函数。如下: export?default?{ ????data?()?{???????? ????return?{??????????? ?????text:?‘some?texts‘,??????? ??????????arr:?[],??????????? ???????????obj:?{} ????????} ????} } Vue为数据中的每一个key维护一个订阅者列表。对于生成的数据,通过Object.defineProperty对其中的每一个key进行处理,主要是为每一个key设置get,set方法,以此来为对应的key收集订阅者,并在值改变时通知对应的订阅者。部分代码如下: ??const?dep?=?new?Dep()??const?property?=?Object.getOwnPropertyDescriptor(obj,?key)??if?(property?&&?property.configurable?===?false)?{????return ??}??//?cater?for?pre-defined?getter/setters ??const?getter?=?property?&&?property.get ??const?setter?=?property?&&?property.set ??let?childOb?=?observe(val) ??Object.defineProperty(obj,?key,?{ ????enumerable:?true,????configurable:?true,????get:?function?reactiveGetter?()?{??????const?value?=?getter???getter.call(obj)?:?val??????if?(Dep.target)?{ ????????dep.depend()????????if?(childOb)?{ ??????????childOb.dep.depend() ????????}????????if?(Array.isArray(value))?{ ??????????dependArray(value) ????????} ??????}??????return?value ????},????set:?function?reactiveSetter?(newVal)?{??????const?value?=?getter???getter.call(obj)?:?val??????/*?eslint-disable?no-self-compare?*/ ??????if?(newVal?===?value?||?(newVal?!==?newVal?&&?value?!==?value))?{????????return ??????}??????/*?eslint-enable?no-self-compare?*/ ??????if?(process.env.NODE_ENV?!==?‘production‘?&&?customSetter)?{ ????????customSetter() ??????}??????if?(setter)?{ ????????setter.call(obj,?newVal) ??????}?else?{ ????????val?=?newVal ??????} ??????childOb?=?observe(newVal) ??????dep.notify() ????} ??}) 每一key都有一个订阅者列表 const dep = new Dep() 在为key进行赋值时,如果值发生了改变,则会通知所有的订阅者 dep.notify() 在对key进行取值时,如果Dep.target有值,除正常的取值操作外会进行一些额外的操作来添加订阅者。大多数时间里,Dep.target的值都为null,只有订阅者在进行订阅操作时,Dep.target才有值,为正在进行订阅的订阅者。此时进行取值操作,会将订阅者加入到对应的订阅者列表中。 订阅者在进行订阅操作时,主要包含以下3个步骤:
在执行订阅操作后,订阅者会被加入到相关key的订阅者列表中。 针对对象和数组的处理如果为key赋的值为对象:
如果为key赋的值为数组:
对模板的处理Vue将模板处理成一个render函数。需要重新渲染DOM时,render函数结合Vue实例中的数据生成一个虚拟节点。新的虚拟节点和原虚拟节点进行对比,对需要修改的DOM节点进行修改。 订阅者订阅者在初始化时主要接受2个参数getter,callback。getter用来计算订阅者的值,所以其在执行时会对订阅者所有需要订阅的key进行取值。订阅者的订阅操作主要是通过getter来实现。 部分代码如下: ??/** ???*?Evaluate?the?getter,?and?re-collect?dependencies. ???*/ ??get?()?{ ????pushTarget(this)????let?value ????const?vm?=?this.vm????if?(this.user)?{??????try?{????????value?=?this.getter.call(vm,?vm) ??????}?catch?(e)?{ ????????handleError(e,?vm,?`getter?for?watcher?"${this.expression}"`) ??????} ????}?else?{??????value?=?this.getter.call(vm,?vm) ????}????//?"touch"?every?property?so?they?are?all?tracked?as ????//?dependencies?for?deep?watching ????if?(this.deep)?{ ??????traverse(value) ????} ????popTarget()????this.cleanupDeps()????return?value ??} 主要步骤:
此后,订阅者在依赖的key的值发生变化会得到通知。获得通知的订阅者并不会立即被触发,而是会被加入到一个待触发的数组中,在下一个周期统一被触发。 订阅者在被触发时,会执行getter来计算订阅者的值,如果值改变,则会执行callback. 负责渲染DOM的订阅者Vue实例化后都会生成一个用于渲染DOM的订阅者。此订阅者在实例化时传入的getter方法为渲染DOM的方法。 部分代码如下: updateComponent?=?()?=>?{ ??vm._update(vm._render(),?hydrating) } vm._watcher?=?new?Watcher(vm,?updateComponent,?noop) vm._render()结合模板和数据,计算出虚拟DOM vm._update()根据虚拟DOM渲染真实的DOM节点 此订阅者在初始化时就会进行订阅操作。实例化时传入的getter为updateComponent。其中的vm._render()在执行时一定会对所有依赖的key进行取值,能完成对依赖的key的订阅。同时vm._update()完成了第一次DOM渲染。当前依赖的key的值发生变化,订阅者被触发时,作为getter的updateComponent会重新执行,重新渲染DOM。因为getter返回的值一直为undefined,所以此订阅者中的callback并没有被用到,于是传入了一个空函数noop作为callback 对computed的处理通过computed可以定义一组计算属性,通过计算属性可以将一些复杂的计算过程抽离出来,保持模板的简单和清晰。 代码示例: export?default?{ ????data?()?{????????return?{????????????text:?‘some?texts‘,????????????arr:?[],????????????obj:?{} ????????} ????},????computed:?{????????key1:?function?()?{????????????return?this.text?+?this.arr.length ????????} ????} } 在定义一个计算属性时,需要定义一个key和一个计算方法。 Vue在对computed进行处理时,会为每一个计算属性生成一个lazy状态的订阅者。普通的订阅者在实例化和触发时会执行getter来计算自身的值和进行订阅操作。而lazy状态的订阅者在上述情况下只会将自身置为dirty状态,不进行其它操作。在订阅者执行自身的evaluate方法时,会清除自身的dirty状态并执行getter来计算自身的值和进行订阅。 Vue在为计算属性生成订阅者时的示例代码如下: const?computedWatcherOptions?=?{?lazy:?true?}//?create?internal?watcher?for?the?computed?property.watchers[key]?=?new?Watcher(vm,?getter,?noop,?computedWatcherOptions) 传入的getter为自定义的计算方法,callback为空函数。(lazy状态的订阅者永远都没有机会执行callback) Vue 在自身实例上为指定key定义get方法,使可以通过Vue实例获取计算属性的值。 部分代码如下: function?createComputedGetter?(key)?{??return?function?computedGetter?()?{????const?watcher?=?this._computedWatchers?&&?this._computedWatchers[key]????if?(watcher)?{??????if?(watcher.dirty)?{ ????????watcher.evaluate() ??????}??????if?(Dep.target)?{ ????????watcher.depend() ??????}??????return?watcher.value ????} ??} } 在对计算属性定义的key进行取值时,会首先获取之前生成好的订阅者。只有订阅者处于dirty状态时,才会执行evaluate计算订阅者的值。所以为计算属性定义的计算方法只有在对计算属性的key进行取值并且计算属性依赖的key曾经改变时才会执行。 假如对上文定义的计算属性key1进行取值 vm.key1;?//第一次取值,自定义计算方法执行vm.key1;?//第二次取值,依赖的key的值没有变化,自定义计算方法不会执行vm.text?=?‘‘?//改变计算属性依赖的key的值,计算属性对应的订阅者会进入dirty状态,自定义计算方法不会执行vm.key1;?//第三次取值,计算属性依赖的key的值发生了变化并且对计算属性进行取值,自定义的计算方法执行 订阅计算属性值的变化计算属性的key不会维护一个订阅者列表,也不能通过计算属性的set方法在触发所有订阅者。(计算属性不能被赋值)。一个订阅者执行订阅操作来订阅计算属性值的变化其实是订阅了计算属性依赖的key的值的变化。 在计算属性的get方法中 if?(Dep.target)?{????watcher.depend()} 如果有订阅者来订阅计算属性的变化,计算属性会将自己的订阅复制到正在进行订阅的订阅者上。watcher.depend()的作用就是如此。 例如: //初始化订阅者watcher,?依赖计算属性key1var?watcher?=?new?Watcher(function?()?{????return?vm.key1 },?noop) vm.text?=?‘‘?//计算属性key1依赖的text的值发生变化,watcher会被触发 对watch的处理Vue实例化时可以传入watch对象,来监听某些值的变化。 例如: export?default?{ ????watch:?{????????‘a.b.c‘:?function?(val,?oldVal)?{????????????console.log(val)????????????console.log(oldVal) ????????} ????} } Vue 会为watch中的每一项生成一个订阅者。订阅者的getter通过处理字符串得到。如‘a.b.c‘会被处理成 function?(vm)?{????var?a?=?vm.a????var?b?=?a.b????var?c?=?b.c????return?c } 处理字符串的源码如下: /** ?*?Parse?simple?path. ?*/const?bailRE?=?/[^w.$]/export?function?parsePath?(path:?string):?any?{??if?(bailRE.test(path))?{????return ??}??const?segments?=?path.split(‘.‘)??return?function?(obj)?{????for?(let?i?=?0;?i?<?segments.length;?i++)?{??????if?(!obj)?return ??????obj?=?obj[segments[i]] ????}????return?obj ??} } 订阅者的callback为定义watch时传入的监听函数。当订阅者被触发时,如果订阅者的值发生变化,则会执行callback。callback执行时会传入变化后的值,变化前的值作为参数。 网易云免费体验馆,0成本体验20+款云产品!? 更多网易技术、产品、运营经验分享请点击。 相关文章: (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |