深入理解 Vue Computed 计算属性
Computed 计算属性是 Vue 中常用的一个功能,但你理解它是怎么工作的吗?
拿官网简单的例子来看一下:
1<div id="example">
2 <p>Original message: "{{ message }}"</p>
3 <p>Computed reversed message: "{{ reversedMessage }}"</p>
4</div>
1var vm = new Vue({
2 el: '#example',
3 data: {
4 message: 'Hello',
5 },
6 computed: {
7 // a computed getter
8 reversedMessage: function () {
9 // `this` points to the vm instance
10 return this.message.split('').reverse().join('');
11 },
12 },
13});
Situation
Vue 里的 Computed 属性非常频繁的被使用到,但并不是很清楚它的实现原理。比如:计算属性如何与属性建立依赖关系?属性发生变化又如何通知到计算属性重新计算?
关于如何建立依赖关系,我的第一个想到的就是语法解析,但这样太浪费性能,因此排除,第二个想到的就是利用 JavaScript 单线程的原理和 Vue 的 Getter 设计,通过一个简单的发布订阅,就可以在一次计算属性求值的过程中收集到相关依赖。
因此接下来的任务就是从 Vue 源码一步步分析 Computed 的实现原理。
Task
分析依赖收集实现原理,分析动态计算实现原理。
Action
data 属性初始化 getter setter:
1// src/observer/index.js
2
3// 这里开始转换 data 的 getter setter,原始值已存入到 __ob__ 属性中
4Object.defineProperty(obj, key, {
5 enumerable: true,
6 configurable: true,
7 get: function reactiveGetter () {
8 const value = getter ? getter.call(obj) : val
9 // 判断是否处于依赖收集状态
10 if (Dep.target) {
11 // 建立依赖关系
12 dep.depend()
13 ...
14 }
15 return value
16 },
17 set: function reactiveSetter (newVal) {
18 ...
19 // 依赖发生变化,通知到计算属性重新计算
20 dep.notify()
21 }
22})
computed 计算属性初始化
1// src/core/instance/state.js
2
3// 初始化计算属性
4function initComputed (vm: Component, computed: Object) {
5 ...
6 // 遍历 computed 计算属性
7 for (const key in computed) {
8 ...
9 // 创建 Watcher 实例
10 // create internal watcher for the computed property.
11 watchers[key] = new Watcher(vm, getter || noop, noop, computedWatcherOptions)
12
13 // 创建属性 vm.reversedMessage,并将提供的函数将用作属性 vm.reversedMessage 的 getter,
14 // 最终 computed 与 data 会一起混合到 vm 下,所以当 computed 与 data 存在重名属性时会抛出警告
15 defineComputed(vm, key, userDef)
16 ...
17 }
18}
19
20export function defineComputed (target: any, key: string, userDef: Object | Function) {
21 ...
22 // 创建 get set 方法
23 sharedPropertyDefinition.get = createComputedGetter(key)
24 sharedPropertyDefinition.set = noop
25 ...
26 // 创建属性 vm.reversedMessage,并初始化 getter setter
27 Object.defineProperty(target, key, sharedPropertyDefinition)
28}
29
30function createComputedGetter (key) {
31 return function computedGetter () {
32 const watcher = this._computedWatchers && this._computedWatchers[key]
33 if (watcher) {
34 if (watcher.dirty) {
35 // watcher 暴露 evaluate 方法用于取值操作
36 watcher.evaluate()
37 }
38 // 同第1步,判断是否处于依赖收集状态
39 if (Dep.target) {
40 watcher.depend()
41 }
42 return watcher.value
43 }
44 }
45}
无论是属性还是计算属性,都会生成一个对应的 watcher 实例。
1// src/core/observer/watcher.js
2
3// 当通过 vm.reversedMessage 获取计算属性时,就会进到这个 getter 方法
4get () {
5 // this 指的是 watcher 实例
6 // 将当前 watcher 实例暂存到 Dep.target,这就表示开启了依赖收集任务
7 pushTarget(this)
8 let value
9 const vm = this.vm
10 try {
11 // 在执行 vm.reversedMessage 的函调函数时,会触发属性(步骤1)和计算属性(步骤2)的 getter
12 // 在这个执行过程中,就可以收集到 vm.reversedMessage 的依赖了
13 value = this.getter.call(vm, vm)
14 } catch (e) {
15 if (this.user) {
16 handleError(e, vm, `getter for watcher "${this.expression}"`)
17 } else {
18 throw e
19 }
20 } finally {
21 if (this.deep) {
22 traverse(value)
23 }
24 // 结束依赖收集任务
25 popTarget()
26 this.cleanupDeps()
27 }
28 return value
29}
上面多出提到了 dep.depend, dep.notify, Dep.target,那么 Dep 究竟是什么呢?
Dep 的代码短小精悍,但却承担着非常重要的依赖收集环节。
1// src/core/observer/dep.js
2
3export default class Dep {
4 static target: ?Watcher;
5 id: number;
6 subs: Array<Watcher>;
7
8 constructor() {
9 this.id = uid++;
10 this.subs = [];
11 }
12
13 addSub(sub: Watcher) {
14 this.subs.push(sub);
15 }
16
17 removeSub(sub: Watcher) {
18 remove(this.subs, sub);
19 }
20
21 depend() {
22 if (Dep.target) {
23 Dep.target.addDep(this);
24 }
25 }
26
27 notify() {
28 const subs = this.subs.slice();
29 for (let i = 0, l = subs.length; i < l; i++) {
30 // 更新 watcher 的值,与 watcher.evaluate() 类似,
31 // 但 update 是给依赖变化时使用的,包含对 watch 的处理
32 subs[i].update();
33 }
34 }
35}
36
37// 当首次计算 computed 属性的值时,Dep 将会在计算期间对依赖进行收集
38Dep.target = null;
39const targetStack = [];
40
41export function pushTarget(_target: Watcher) {
42 // 在一次依赖收集期间,如果有其他依赖收集任务开始(比如:当前 computed 计算属性嵌套其他 computed 计算属性),
43 // 那么将会把当前 target 暂存到 targetStack,先进行其他 target 的依赖收集,
44 if (Dep.target) targetStack.push(Dep.target);
45 Dep.target = _target;
46}
47
48export function popTarget() {
49 // 当嵌套的依赖收集任务完成后,将 target 恢复为上一层的 Watcher,并继续做依赖收集
50 Dep.target = targetStack.pop();
51}
Result
总结一下依赖收集、动态计算的流程:
1. data 属性初始化 getter setter
2. computed 计算属性初始化,提供的函数将用作属性 vm.reversedMessage 的 getter
3. 当首次获取 reversedMessage 计算属性的值时,Dep 开始依赖收集
4. 在执行 message getter 方法时,如果 Dep 处于依赖收集状态,则判定 message 为 reversedMessage 的依赖,并建立依赖关系
5. 当 message 发生变化时,根据依赖关系,触发 reverseMessage 的重新计算
到此,整个 Computed 的工作流程就理清楚了。
Vue 是一个设计非常优美的框架,使用 Getter Setter 设计使依赖关系实现的非常顺其自然,使用计算与渲染分离的设计(优先使用 MutationObserver,降级使用 setTimeout)也非常贴合浏览器计算引擎与排版引擎分离的的设计原理。
如果你想成为一名架构师,不能只停留在框架的 API 使用层面。