Vue computed
一说起 computed,提到最多的就是:计算属性是惰性求值的,而且计算属性的值是会被缓存的,只有当依赖的响应式数据更新后才会被重新计算求值。
那你是否真的了解这其中的缘由?又是不是想过:
- 计算属性改变后,是如何通知页面重新渲染的?
- 与计算属性相关联的响应式数据,又是通过什么方式将
计算属性的 watcher 实例
添加进自己的观察者队列
的呢?
# computed
在组件初始化过程中,跟 props、data、watch 类似,initState 内部也会对组件内的 computed 属性进行初始化。
抛开 SSR 场景,以下面的用法为例:
{
data() {
return {
a: 1,
b: 2
}
},
computed: {
c() {
return this.a + this.b
}
},
}
2
3
4
5
6
7
8
9
10
11
12
13
# 初始化
// 初始化 computed
function initComputed(vm: Component, computed: Object) {
// 创建的watcher实例会被存储到组件实例的 `_computedWatchers` 对象中
const watchers = (vm._computedWatchers = Object.create(null))
// 遍历所有计算属性的定义
for (const key in computed) {
const userDef = computed[key]
const getter = typeof userDef === 'function' ? userDef : userDef.get
// ========= 会为每个computed key 创建一个watcher实例 ========= //
// ====== computed 的本质就是一个 `lazy: true` 的 Watcher 实例 ======== //
watchers[key] = new Watcher(vm, getter || noop, noop, { lazy: true })
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 惰性求值
在初始化过程中,由于 { lazy: true }
,所以计算属性的 watcher 并不会立即调用 getter 进行求值,这就是 惰性求值
的第一个表现。
这里的getter
就是示例中 function c() { return this.a + this.b }
这个方法
// 代码有删减
class Watcher {
constructor(vm, expOrFn, cb, options, isRenderWatcher) {
this.vm = vm
vm._watchers.push(this)
// ...
this.dirty = this.lazy = !!options.lazy
// ...
this.getter = expOrFn
// ...
// 计算属性的 watcher ,它的 lazy 属性为 true
this.value = this.lazy ? undefined : this.get()
}
get() {
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
popTarget()
return value
}
// 依赖的数据变更后,调用该方法
update() {
if (this.lazy) {
// 这里会将 dirty 置为 true
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
evaluate() {
this.value = this.get()
// 这里会将 dirty 置为 false
this.dirty = false
}
}
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
# dirty
在 Watcher 类中可以看到,lazy
一旦赋值就再也不会改变,而 dirty
虽然初始化时取了 lazy 属性的值,但这个值是会发生改变的,改变的时机有两个:
- 在
evaluate
方法内,执行完 getter 进行求值后,会将该值置为 false - 在
update
方法内,如果 lazy 属性为 true 的时候,则会将该值再置为 true
这里可以先说下 dirty 的作用:
computed 属性除了是惰性求值的外,它的值还是可以被缓存的,而 dirty
作用就是用来标识当前的缓存值是否依然有效,
- 如果为
false
,说明缓存可用,无需调用getter
重新计算 - 如果为
true
,说明缓存数据'脏了'
,需要重新计算
说到这里,应该可以明白,为什么是在依赖更新后将 dirty 置为 true,而在调用 getter 重新求值后又置为 false 了
接着深入看下evaluate
以及update
这两个方法究竟是在什么时候被调用的
# 求值
computed 计算属性的定义其实除了文章开头的 function 的方式,还可以以对象的方式,单独设置 getter 和 setter:
{
computed: {
c: {
// getter
get: function () {
return this.a + this.b
},
// setter
set: function (newValue) {
var values = newValue.split(' ')
this.a = values[0]
this.b = values[values.length - 1]
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
初始化过程中的最后,会根据当前计算属性的 key 是否在当前组件实例上存在,进行 computed 属性的 getter 和 setter 的配置工作,
function initComputed() {
// ...
// 如果 key 在当前实例上不存在
if (!(key in vm)) {
defineComputed(vm, key, userDef)
}
}
2
3
4
5
6
7
8
# defineComputed
// 代码有删减
function defineComputed(target, key, userDef) {
// computed 属性值是一个函数
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
} else {
// 是一个对象
sharedPropertyDefinition.get = userDef.get
? createComputedGetter(key)
: noop
sharedPropertyDefinition.set = userDef.set || noop
}
// ...
Object.defineProperty(target, key, sharedPropertyDefinition)
}
function createComputedGetter(key) {
// 这个就是计算属性的 getter,在访问计算属性的值的时候会被触发
return function computedGetter() {
// 取出对应的观察者
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 执行getter
watcher.evaluate()
}
// ...
return watcher.value
}
}
}
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
重点在 createComputedGetter
方法中返回的计算属性的真正 getter。还是以开头的例子来看,假设在模板插值中用到了计算属性 c
的值,此时就会调用这里的 computedGetter
方法
# 初次求值
computedGetter
方法首先从组件实例的_computedWatchers
属性中获取到属性c
的watcher
实例(这里是一对一的关系)。如果不存在则无需求值- 存在的话,会根据
watcher.dirty
的值决定是否进行求值操作。初次渲染该值为 true,所以这里会执行evaluate
方法进行求值操作- 求得的值被缓存在 watcher 实例的 value 属性上,同时将
this.dirty = false
- 求得的值被缓存在 watcher 实例的 value 属性上,同时将
- 最后返回了这个求得的值
这样的话,由于 dirty
是 false,所以即便 c
后续又被多次访问到,也不会被多次求值,而是直接返回 watcher.value
缓存的结果
这里还有很重要的一步,就是对于计算属性 c 的 watcher 实例,对 c 所依赖的 a 和 b 的依赖收集过程。简单地说,就是属性 a 和属性 b 如何将计算属性 c 的 watcher 实例,添加到自己的观察者队列中来,以便在自己更新后通知到 c 去重新计算求值。
这个过程稍微有点复杂,一步步看:
# 依赖收集
- 上面提到初次渲染时,会执行
evaluate
方法对 c 进行求值操作,也就意味着计算属性 c 的 watcher 实例的 get 方法会被执行。所以这里pushTarget(this)
中的 this 就是计算属性 c 的 watcher 实例。此时Dep.target = this
get() {
pushTarget(this)
const vm = this.vm
let value = this.getter.call(vm, vm)
popTarget()
return value
}
2
3
4
5
6
7
8
9
this.getter
执行,也就是function c() { return this.a + this.b }
这个方法被执行。所以这里又会触发属性 a 和 属性 b 的 getter。以 a 为例:
// a 的 getter
{
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 依赖收集
if (Dep.target) {
dep.depend()
// ...
}
return value
}
}
2
3
4
5
6
7
8
9
10
11
12
由于 Dep.target
存在,所以会进入到属性 a 的依赖收集过程 dep.depend()
,这样一来就将计算属性 c 的 watcher 实例添加进属性 a 的 watcher 队列中去了,属性 b 同理。
那接下来就看下当依赖的属性 a 或者 b 发生更新后,计算属性 c 又是如何被重新计算求值的。
# 重新求值
假如有下列场景:a 的值被重新赋值为 20
this.a = 20
由于 a 的值更新了,所以所有观察了该值的 watcher 实例的 update 方法将会被调用,所以此时计算属性 c 的 watcher 实例的 update 方法会被调用:
- 因为计算属性的
lazy === true
,所以 dirty 属性又被置为了 true,此时同样不会对 c 进行求值
// 依赖的数据变更后,调用该方法
update() {
if (this.lazy) {
// 这里会将 dirty 置为 true
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
2
3
4
5
6
7
8
9
10
11
- 而当应用中,再次访问到 c 的时候,c 的 getter 会被执行,由于 dirty 属性为 true,所以此时就会对 c 进行重新求值
function computedGetter() {
// 取出对应的观察者
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 执行getter,进行求值
watcher.evaluate()
}
// ...
return watcher.value
}
}
2
3
4
5
6
7
8
9
10
11
12
到这里就解答了文章一开始说到的惰性求值
、值会被缓存
以及计算属性的 watcher 实例是如何被添加进其依赖数据的观察者队列中去的
几个问题。还剩下最后一个问题,就是当计算属性的值更新之后,如何通知渲染 watcher 去重新渲染页面呢?
# 通知渲染 Watcher
这一部分更加的绕,建议不太熟悉响应式原理部分源码的同学可以结合源码一点点看。
上面提到,在计算属性 c 的求值过程会触发属性 a 和 b 的 getter,
// a 的 getter
{
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
// 依赖收集
if (Dep.target) {
dep.depend()
// ...
}
return value
}
}
2
3
4
5
6
7
8
9
10
11
12
还是以 a 为例,来看下 dep.depend()
的过程中都做了什么
# dep.depend
class Dep {
depend() {
if (Dep.target) {
Dep.target.addDep(this)
}
}
}
2
3
4
5
6
7
此时 Dep.target 指向的是 计算属性 c 的 watcher 实例,所以
class Watcher {
// ...
addDep(dep: Dep) {
// ...
this.newDeps.push(dep)
// ...
}
}
2
3
4
5
6
7
8
经过 addDep 之后,计算属性 c 的 watcher 实例的 newDeps 属性中就有了 a 和 b 这两个依赖。
然后回到计算属性 c 的 getter 中来,其实在计算属性 c 的 watcher 实例存在的时候,里面的逻辑还有一步,就是当 Dep.target
有值时,会调用计算属性 c 的 watcher 实例的 depend 方法,
function computedGetter() {
// 取出对应的观察者
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
2
3
4
5
6
7
8
9
10
11
12
13
那此时的 Dep.target 又指向谁呢?还是计算属性 c 的 watcher 实例吗?
很显然不是,因为当
watcher.evaluate()
执行结束后,此时计算属性 c 的 watcher 实例的 get 方法 已经执行完毕,也就意味着下面的 popTarget 已经将计算属性 c 的 watcher 实例弹出了栈外。
get() {
pushTarget(this)
let value
try {
const vm = this.vm
value = this.getter.call(vm, vm)
} catch(e) {
// ...
} finally {
popTarget()
this.cleanupDeps()
}
return value
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# watcher.cleanupDeps
另外,还要再提一下在 finally 块中,cleanupDeps 的操作:这里会将计算属性 c 的 watcher 实例 deps 属性赋值为 newDeps,也就是此时 deps 中也保存了 a 和 b 的两个 dep 实例。
class Watcher {
cleanupDeps() {
// ...
this.deps = this.newDeps
// ...
}
}
2
3
4
5
6
7
当计算属性 c 的 watcher 实例被弹出之后,此时栈内还有谁呢?那就是组件的渲染 watcher。至于为什么,大家可以去了解下组件的挂载过程,这里不再赘述。
接下来就看下 if (Dep.target) { watcher.depend() }
中,计算属性 c 的 watcher 实例的 depend 方法又做了什么
# watcher.depend
depend() {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
2
3
4
5
6
由于 this.cleanupDeps()
方法的执行,将属性 a 和 b 的 dep 实例都放到了计算属性 c 的 watcher 实例的 dep 队列中,所以这里会遍历这个队列,并依次调用了它们的 depend 方法
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
2
3
4
5
记住这里的 Dep.target 已经指向了 当前组件的渲染 watcher,所以最终Dep.target.addDep(this)
的执行结果就是:
- 在 a 的 watcher 队列中放入渲染 watcher
- 在 b 的 watcher 队列中放入渲染 watcher
这样一来,无论是 a 还是 b 发生了变化,最终都会通知到页面渲染 watcher 来重新渲染页面。这里就解答了文章一开始提到的最后一个问题,计算属性改变后,是如何通知页面重新渲染的。而且 页面渲染 watcher 与 计算属性 c 的入栈顺序保证了,当页面重新渲染后,始终得到的都是计算属性 c 的最新值。
# 总结
- 计算属性与普通的 data 中的属性不同,计算属性并没有对应的 Dep 实例,也没有对应的依赖收集过程。
- 计算属性的本质是一个惰性求值的 watcher 实例,它所依赖的数据,会将这个 watcher 实例添加进自身的观察者队列中去,以便在其依赖的数据变更后,通知它的 watcher 实例进行重新求值
- 计算属性重新求值后,也是通过其依赖的数据的观察者队列中的渲染 watcher 来重新渲染页面的,它自身跟渲染 watcher 无关