July 23, 2023

Vue2源码(5):计算属性原理

内心旁白:计算属性有点绕呀,看了好几遍了

1 计算属性

首先 Vue 中定义了 3 个 watcher

render watcher 之前的文章已经分析过了

下面会分析一下 computed watcher 的实现原理

2 计算属性的实现

一个计算属性的实现分为2部分

2.1 创建 computed watcher

2.1.1 计算属性的两种写法的差异

首先计算属性有两种写法:直接写为 function 和 完整写法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
computed: {
// defineProperty中的get方法
fullname () {
return this.firstname + this.lastname
}
// 完整写法
fullname:{
get(){
console.log('run')
return this.firstname + this.lastname
},
set(newVal){
console.log(newVal)
}
}
}

如何求得 fullname 值,第一种方法可以拿来直接用,第二种需要拿到它的get

2.1.2 实例化一个 computed watcher

源码路径:vue-main\src\core\instance\state.ts

在初始化当前组件时,会执行 initComputed 方法初始化计算属性

首先,该方法中,首先遍历用户写的计算属性

拿到计算 fullname 属性的方法:

1
2
// 计算属性的多种写法: 直接写function /  完整写法,包含get、set
const getter = isFunction(userDef) ? userDef : userDef.get

然后,会进行实例化

1
2
3
4
5
6
7
8
const computedWatcherOptions = { lazy: true }

watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)

new Watcher 时,传入用户定义的求值函数(getter),并传入 lazy: true 表示该 watcher 是 computed watcher

2.1.3 重新劫持计算属性的 get

模板读取计算属性时,需要有计算属性自己的求值逻辑,所以要劫持它的 get

createComputedGetter 方法重新定义了计算属性的 get

当计算属性 fullname 求值时,会走这里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function createComputedGetter(key) {
return function computedGetter() {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
// 求新值
if (watcher.dirty) {
watcher.evaluate()
}
// 让依赖的属性 也去渲染watcher
if (Dep.target) {
if (__DEV__ && Dep.target.onTrack) {
Dep.target.onTrack({
effect: Dep.target,
target: this,
type: TrackOpTypes.GET,
key
})
}
watcher.depend()
}
return watcher.value
}
}
}

这个方面里面也涉及到了 实现计算属性 的几个关键变量和方法

dirty

计算属性使用 dirty 属性来标记该数据是否是脏数据

如果是脏数据,需要重新计算值

否则可以直接使用旧值

dirty 默认为 true

watcher.evaluate()

evaluate() 是计算属性专用的方法,其实方法很简单

首先使用 watcher 用来求值的通用方法: get() (计算属性入栈、求值、出栈)

然后将 dirty 标记:不是脏数据

1
2
3
4
evaluate() {
this.value = this.get()
this.dirty = false
}

watcher.depend()

计算属性watcher中依赖的属性 也去收集栈中的其他watcher

它会遍历当前 computed watcher 的deps属性,依次执行 dep 的 depend 方法

知道了几个重要概念,下面举个例子更容易理解它的整体实现逻辑

3 举个栗子

举例子之前首先得明确3点:

3.1 初始化

1
2
3
4
5
6
定义计算属性:
computed: {
fullname () {
return this.firstname + this.lastname
}
}

首先,组件初始化时,已经有一个 render watcher 了。

dep 中 targetStack 栈会维护着 [render watcher]

然后,实例化一个 computed watcher,computed watcher 不会执行默认的求值操作

当模板中使用计算属性时

1
{{ fullname }}

才会执行求值操作,这时计算属性独有 getter 拦截,因为 dirty 默认为 true,会进入 watcher.evaluate() 方法

watcher.evaluate() 方法做了两件事:get()、将 dirty 置为false

watcher通用求值方法,求fullname

计算属性入栈,targetStack: [render watcher, computed watcher],Dep.target 指向 computed watcher

调用回调函数(用户定义的计算属性函数)求 fullname 值,也就会被 firstname 和 lastname 的 getter 劫持

firstname 和 lastname 的 getterr:也就是会进行依赖收集

firstname 和 lastname 的 dep 会新增computed watcher(因为Dep.target 指向 computed watcher)

计算属性出栈

到这里求值步骤就结束了

但前面说过了

computed watcher的工作只是求值,标记dirty

render watcher才会真正更新视图

所以应该让 firstname 和 lastname 的 dep 也去收集 render watcher,当依赖的属性变化时能做到更新渲染

所以 computed watcher 的 getter 劫持中,watcher.depend() 方法就起作用了

让 firstname 和 lastname 也去收集 render watcher

所以说:计算属性(fullname)根本不会收集依赖,而是让它依赖的属性(firstname、lastname)去收集依赖

3.2 依赖的属性发生变化

1
2
3
setTimeout(() => {
vm.firstname = '新值';
}, 1000)

只有当计算属性的依赖项被修改时,计算属性才会重新进行计算,生成一个新的值,而视图中其他变量被修改导致视图更新时,计算属性不会重新计算,这是怎么做到的呢?

当计算属性的依赖项,即 firstName 和 lastName 被修改时,数据劫持会触发内部的 setter,执行 watcher 的 update

初始化时,前面的 evaluatedepend 方法,firstName 和 lastName 内部的 dep 中都会保存 2 个 watcher,一个 computed watcher,一个 render watcher

**所以,首先会执行 computed watcher 的 update()**
1
2
3
4
5
6
7
8
9
10
update() {
if (this.lazy) {
// 将 dirty 标记为脏数据
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

属于计算属性watcher, lazy 为 true

因此会将 dirty 标记为脏数据

可以看到,computed watcher就做了这么一个简单的事情,将 dirty 标记为脏数据

真正的求值操作是在 render watcher 中进行的:

执行 render watcher 时

由于视图依赖了 fullName,会触发计算属性重新定义的 getter,执行前面的 createComputedGetter

由于 dirty 为 true 脏数据,则会进行求值逻辑:computed watcher 的 watcher.evaluate()

此时 fullName 就拿到了最新的值了

3.3 非依赖的属性发生变化

如果我修改的是与 fullname 无关的属性

1
vm.otherTimes = '新值';

因为这个属性不会触发计算watcher的

1
2
3
4
if (this.lazy) {
// 将 dirty 标记为脏数据
this.dirty = true
}

因为 dirty 为 false,所以 fullname 不会重新执行计算新值的方法:watcher.evaluate()

4 流程图

About this Post

This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.