- 7.10 computed
- 7.10.1 依赖收集
- 7.10.2 派发更新
7.10 computed
计算属性设计的初衷是用于简单运算的,毕竟在模板中放入太多的逻辑会让模板过重且难以维护。在分析computed时,我们依旧遵循依赖收集和派发更新两个过程进行分析。
7.10.1 依赖收集
computed的初始化过程,会遍历computed的每一个属性值,并为每一个属性实例化一个computed watcher,其中{ lazy: true}是computed watcher的标志,最终会调用defineComputed将数据设置为响应式数据,对应源码如下:
function initComputed() {···for(var key in computed) {watchers[key] = new Watcher(vm,getter || noop,noop,computedWatcherOptions);}if (!(key in vm)) {defineComputed(vm, key, userDef);}}// computed watcher的标志,lazy属性为truevar computedWatcherOptions = { lazy: true };
defineComputed的逻辑和分析data的逻辑相似,最终调用Object.defineProperty进行数据拦截。具体的定义如下:
function defineComputed (target,key,userDef) {// 非服务端渲染会对getter进行缓存var shouldCache = !isServerRendering();if (typeof userDef === 'function') {//sharedPropertyDefinition.get = shouldCache? createComputedGetter(key): createGetterInvoker(userDef);sharedPropertyDefinition.set = noop;} else {sharedPropertyDefinition.get = userDef.get? shouldCache && userDef.cache !== false? createComputedGetter(key): createGetterInvoker(userDef.get): noop;sharedPropertyDefinition.set = userDef.set || noop;}if (sharedPropertyDefinition.set === noop) {sharedPropertyDefinition.set = function () {warn(("Computed property \"" + key + "\" was assigned to but it has no setter."),this);};}Object.defineProperty(target, key, sharedPropertyDefinition);}
在非服务端渲染的情形,计算属性的计算结果会被缓存,缓存的意义在于,只有在相关响应式数据发生变化时,computed才会重新求值,其余情况多次访问计算属性的值都会返回之前计算的结果,这就是缓存的优化,computed属性有两种写法,一种是函数,另一种是对象,其中对象的写法需要提供getter和setter方法。
当访问到computed属性时,会触发getter方法进行依赖收集,看看createComputedGetter的实现。
function createComputedGetter (key) {return function computedGetter () {var watcher = this._computedWatchers && this._computedWatchers[key];if (watcher) {if (watcher.dirty) {watcher.evaluate();}if (Dep.target) {watcher.depend();}return watcher.value}}}
createComputedGetter返回的函数在执行过程中会先拿到属性的computed watcher,dirty是标志是否已经执行过计算结果,如果执行过则不会执行watcher.evaluate重复计算,这也是缓存的原理。
Watcher.prototype.evaluate = function evaluate () {// 对于计算属性而言 evaluate的作用是执行计算回调this.value = this.get();this.dirty = false;};
get方法前面介绍过,会调用实例化watcher时传递的执行函数,在computer watcher的场景下,执行函数是计算属性的计算函数,他可以是一个函数,也可以是对象的getter方法。
列举一个场景避免和
data的处理脱节,computed在计算阶段,如果访问到data数据的属性值,会触发data数据的getter方法进行依赖收集,根据前面分析,data的Dep收集器会将当前watcher作为依赖进行收集,而这个watcher就是computed watcher,并且会为当前的watcher添加访问的数据Dep
回到计算执行函数的this.get()方法,getter执行完成后同样会进行依赖的清除,原理和目的参考data阶段的分析。get执行完毕后会进入watcher.depend进行依赖的收集。收集过程和data一致,将当前的computed watcher作为依赖收集到数据的依赖收集器Dep中。
这就是computed依赖收集的完整过程,对比data的依赖收集,computed会对运算的结果进行缓存,避免重复执行运算过程。
7.10.2 派发更新
派发更新的条件是data中数据发生改变,所以大部分的逻辑和分析data时一致,我们做一个总结。
- 当计算属性依赖的数据发生更新时,由于数据的
Dep收集过computed watch这个依赖,所以会调用dep的notify方法,对依赖进行状态更新。 - 此时
computed watcher和之前介绍的watcher不同,它不会立刻执行依赖的更新操作,而是通过一个dirty进行标记。我们再回头看依赖更新的代码。
Dep.prototype.notify = function() {···for (var i = 0, l = subs.length; i < l; i++) {subs[i].update();}}Watcher.prototype.update = function update () {// 计算属性分支if (this.lazy) {this.dirty = true;} else if (this.sync) {this.run();} else {queueWatcher(this);}};
由于lazy属性的存在,update过程不会执行状态更新的操作,只会将dirty标记为true。
- 由于
data数据拥有渲染watcher这个依赖,所以同时会执行updateComponent进行视图重新渲染,而render过程中会访问到计算属性,此时由于this.dirty值为true,又会对计算属性重新求值。
