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属性为true
var 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
,又会对计算属性重新求值。