回顾
上回提到,computed————计算属性的缓存与 Watcher 这个类的 dirty 属性有关,那么这次我们接着来看下,dirty 属性到底取决于什么情况来变化,从而对 computed 进行缓存。
依赖收集
切入正题之前,我们先来看一个问题:如果一个 computed 的结果是受 data 属性下的值影响的,那么如何去捕获因某个值变化而引起的 computed 的变化?答案是:依赖收集
根据上面的断点,在 update 函数执行之前,我们注意到,有个 reactiveSetter 函数在它之前。我们点击右侧调用栈中的 reactiveSetter,此时有一个函数特别醒目:defineReactive$$1,经过又一次的断点,我们发现它在几处都有调用:
在 initRender 函数中调用
在 walk 函数中调用
在实际断点调试的时候,我们很容易可以知道存在这样的,同时也是与本文有关的调用顺序(从下往上):
defineReactive$$1
walk
Observer
observe
initData
initState
…
Observer 类
根据上边提供的调用顺序,我们重点看一下几个关键的函数:
observe
/**
* Attempt to create an observer instance for a value,
* returns the new observer if successfully observed,
* or the existing observer if the value already has one.
*/
function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) {
return
}
var ob;
if (hasOwn(value, ‘__ob__’) && value.__ob__ instanceof Observer) {
ob = value.__ob__;
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value);
}
if (asRootData && ob) {
ob.vmCount++;
}
return ob
}
光看注释我们都能知道,observe 函数的作用是:为某个值创建一个 observer 实例,随后将这个 observer 实例返回,在这里起到一个对值进行筛选的作用
Observer
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object’s property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep();
this.vmCount = 0;
def(value, ‘__ob__’, this);
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods);
} else {
copyAugment(value, arrayMethods, arrayKeys);
}
this.observeArray(value);
} else {
this.walk(value);
}
};
注释大意:
每个被观察的对象都附属于 Observer 类。每次对对象的观察都会将它的 getter 和 setter 属性覆盖,用以收集依赖以及触发更新
walk && defineReactive$$1
Observer.prototype.walk = function walk (obj) {
var keys = Object.keys(obj);
for (var i = 0; i < keys.length; i++) {
defineReactive$$1(obj, keys[i]);
}
};
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
var dep = new Dep();
var property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
var getter = property && property.get;
var setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
var 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) {
var 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();
}
// #7981: for accessor properties without setter
if (getter && !setter) {return}
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
}
});
}
其中,这端代码是关键:
get: function reactiveGetter () {
var value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value
},
如果阅读了整段 defineReactive$$1 函数,那么很容易就发现,dep 不过是 Dep 类 new 出来的实例,那么即使不看 Dep.prototype.depend 的实现,你也知道 dep.depend() 其实也就是在收集依赖。另外,这段代码意味着单单在 data 属性下声明一个变量是不会进行依赖收集的,需要变量在程序中被调用,那么才会被收集到依赖中(其实这也是一种优化)
附 Dep 类下的相关实现
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
Dep.prototype.addSub = function addSub (sub) {
this.subs.push(sub);
};
Dep.prototype.removeSub = function removeSub (sub) {
remove(this.subs, sub);
};
Dep.prototype.depend = function depend () {
if (Dep.target) {
Dep.target.addDep(this);
}
};
Dep.prototype.notify = function notify () {
// stabilize the subscriber list first
var subs = this.subs.slice();
if (process.env.NODE_ENV !== ‘production’ && !config.async) {
// subs aren’t sorted in scheduler if not running async
// we need to sort them now to make sure they fire in correct
// order
subs.sort(function (a, b) {return a.id – b.id;});
}
for (var i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
};
总结
上面说了这么多未免有点乱,最后重新梳理下 computed 实现缓存的思路:
Vue 在初始化 data 属性时,会将 data 属性下相关的变量进行观察(observe),同时重新设置它的 getter 和 setter 属性,以便在其被调用时收集到它的依赖。
初始化 computed
调用 computed 时判断 this.dirty 属性,为 true 时调用 evaluate 重新计算它的值并将 this.dirty 置为 false,将值存在 this.value 上,再调用 computed 则直接返回 this.value
当 computed 中依赖的值发生变化时会自动触发该值的 setter 属性,紧接着调用 notify 函数,遍历一个 subs 数组,触发 update 函数将 this.dirty 重置为 true
当 computed 再次被调用时,由于 this.dirty 已经是 true,则会重新计算