Vue之computed实现原理

computed 和 watch

很多时候我们使用 computed 时往往会与 Vue 中另一个 API 也就是侦听器 watch 相比较,因为在某些方面它们是一致的,都是以 Vue 的依赖追踪机制为基础,当某个依赖数据发生变化时,所有依赖这个数据的相关数据或函数都会自动发生变化或调用。

虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的侦听器。这就是为什么 Vue 通过 watch 选项提供了一个更通用的方法来响应数据的变化。当需要在数据变化时执行异步或开销较大的操作时,这个方式是最有用的。

从 Vue 官方文档对 watch 的解释我们可以了解到,使用 watch 选项允许我们执行异步操作(访问一个 API)或高消耗性能的操作,限制我们执行该操作的频率,并在我们得到最终结果前,设置中间状态,而这些都是计算属性无法做到的。

下面还另外总结了几点关于 computed 和 watch 的差异:

  1. computed 是计算一个新的属性,并将该属性挂载到 vm(Vue 实例)上,而 watch 是监听已经存在且已挂载到 vm 上的数据,所以用 watch 同样可以监听 computed 计算属性的变化(其它还有 data、props)
  2. computed 本质是一个惰性求值的观察者,具有缓存性,只有当依赖变化后,第一次访问 computed 属性,才会计算新的值,而 watch 则是当数据发生变化便会调用执行函数
  3. 从使用场景上说,computed 适用一个数据被多个数据影响,而 watch 适用一个数据影响多个数据

原理分析

Vue 响应系统,其核心有三点:observe、watcher、dep:

  1. observe:遍历 data 中的属性,使用 Object.defineProperty 的 get/set 方法对其进行数据劫持;
  2. dep:每个属性拥有自己的消息订阅器 dep,用于存放所有订阅了该属性的观察者对象;
  3. watcher:观察者(对象),通过 dep 实现对响应属性的监听,监听到结果后,主动触发自己的回调进行响应。

Vue 之模板渲染过程中 new Vue 第 3 点[初始化 computed,wacther]。

initState

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export function initState(vm: Component) {
vm._watchers = [];
const opts = vm.$options;
if (opts.props) initProps(vm, opts.props);
if (opts.methods) initMethods(vm, opts.methods);
if (opts.data) {
initData(vm);
} else {
observe((vm._data = {}), true /* asRootData */);
}
// computed初始化
if (opts.computed) initComputed(vm, opts.computed);
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch);
}
}

initComputed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
const computedWatcherOptions = { computed: true };
function initComputed(vm: Component, computed: Object) {
// $flow-disable-line
const watchers = (vm._computedWatchers = Object.create(null));
// computed properties are just getters during SSR
const isSSR = isServerRendering();

for (const key in computed) {
const userDef = computed[key];
const getter = typeof userDef === "function" ? userDef : userDef.get;
if (process.env.NODE_ENV !== "production" && getter == null) {
warn('Getter is missing for computed property "${key}".', vm);
}

if (!isSSR) {
// create internal watcher for the computed property.
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
);
}

// component-defined computed properties are already defined on the
// component prototype. We only need to define computed properties defined
// at instantiation here.
if (!(key in vm)) {
defineComputed(vm, key, userDef);
} else if (process.env.NODE_ENV !== "production") {
if (key in vm.$data) {
warn(
'The computed property "${key}" is already defined in data.',
vm
);
} else if (vm.$options.props && key in vm.$options.props) {
warn(
'The computed property "${key}" is already defined as a prop.',
vm
);
}
}
}
}

Watcher(已移除部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
class Watcher {
constructor(
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
if (options) {
this.computed = !!options.computed;
}

if (this.computed) {
this.value = undefined;
this.dep = new Dep();
} else {
this.value = this.get();
}
}

get() {
pushTarget(this);
let value;
const vm = this.vm;
try {
value = this.getter.call(vm, vm);
} catch (e) {
} finally {
popTarget();
}
return value;
}

update() {
if (this.computed) {
if (this.dep.subs.length === 0) {
this.dirty = true;
} else {
this.getAndInvoke(() => {
this.dep.notify();
});
}
} else if (this.sync) {
this.run();
} else {
queueWatcher(this);
}
}

evaluate() {
if (this.dirty) {
this.value = this.get();
this.dirty = false;
}
return this.value;
}

depend() {
if (this.dep && Dep.target) {
this.dep.depend();
}
}
}

Dep(已移除部分代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
export default class Dep {
static target: ?Watcher;
subs: Array<Watcher>;

constructor() {
this.id = uid++;
this.subs = [];
}

addSub(sub: Watcher) {
this.subs.push(sub);
}

depend() {
if (Dep.target) {
Dep.target.addDep(this);
}
}

notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
Dep.target = null;

defineComputed

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export function defineComputed(
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering();
if (typeof userDef === "function") {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: userDef;
sharedPropertyDefinition.set = noop;
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: userDef.get
: noop;
sharedPropertyDefinition.set = userDef.set ? userDef.set : noop;
}
if (
process.env.NODE_ENV !== "production" &&
sharedPropertyDefinition.set === noop
) {
sharedPropertyDefinition.set = function () {
warn(
'Computed property "${key}" was assigned to but it has no setter.',
this
);
};
}
Object.defineProperty(target, key, sharedPropertyDefinition);
}

看完上面的源码,总结下来就是:

  1. 当组件初始化的时候,computed 和 data 会分别建立各自的响应系统,Observer 遍历 data 中每个属性设置 get/set 数据拦截

  2. 初始化 computed 会调用 initComputed 函数:

    1. 注册一个 watcher 实例,并在内实例化一个 Dep 消息订阅器用作后续收集依赖(比如渲染函数的 watcher 或者其他观察该计算属性变化的 watcher )

    2. 调用计算属性时会触发其 Object.defineProperty 的 get 访问器函数

    3. 调用 watcher.depend() 方法向自身的消息订阅器 dep 的 subs 中添加其他属性的 watcher

    4. 调用 watcher 的 evaluate 方法(进而调用 watcher 的 get 方法)让自身成为其他 watcher 的消息订阅器的订阅者,首先将 watcher 赋给 Dep.target,然后执行 getter 求值函数,当访问求值函数里面的属性(比如来自 data、props 或其他 computed)时,会同样触发它们的 get 访问器函数从而将该计算属性的 watcher 添加到求值函数中属性的 watcher 的消息订阅器 dep 中,当这些操作完成,最后关闭 Dep.target 赋为 null 并返回求值函数结果。

    5. 当某个属性发生变化,触发 set 拦截函数,然后调用自身消息订阅器 dep 的 notify 方法,遍历当前 dep 中保存着所有订阅者 wathcer 的 subs 数组,并逐个调用 watcher 的 update 方法,完成响应更新。

[越努力,越幸运!]