Vue2计算属性的实现原理

ObjectKaz Lv4

Vue2仓库链接:https://github.com/vuejs/vue

本文使用的是 2.6.14 版本的源代码

问题引入

我们来看一个具体的例子:

你可以 点击这里 来在线运行这个例子。

1
2
3
4
<div id="app">
<p>{{sum}}</p>
<button @click="change">+1</button>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
new Vue({
el: "#app",
data() {
return {
count: 1,
}
},
methods: {
change() {
this.count++
},
},
computed: {
sum() {
return this.count + 1
},
},
})

我们现在要研究的是:当 this.count 发生了变化,Vue 是如何实现同时变化 sum 的。

看源码之前,我们最先想到的办法就是直接解析计算属性的函数体代码,但是 js 代码的灵活性注定了这样比较麻烦,而且这么做会产生很多的限制。

如果有听过 CMD 模块规范,可以发现里面有一个要求:不能为 require 定义别名,因为 CMD 模块规范主要靠静态解析代码来获得模块的依赖关系的,如果把 require 改名了,就检测不到了。

Vue2 采用了一种独特的依赖收集的方式,具体内容我们慢慢谈。

初始化计算属性

我们翻到源码的 src/core/instance/state.js,找到 initComputed 这个函数。

这个函数是相当长啊。但要注意,Vue2 的源码是非常庞大的,但实际上 80% 以上的部分都是在处理一些边界性的情况,我们在阅读源码的时候,可以忽略这些细枝末节,只看主要的部分。

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
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)
} else if (vm.$options.methods && key in vm.$options.methods) {
warn(`The computed property "${key}" is already defined as a method.`, vm)
}
}
}
}

我们先看到 for (const key in computed) 这一段,很容易看出这是遍历所有的计算属性。

中间一个 if 语句里面弹出一个警告,先跳过。

接下来定义了一个 Watcher

1
2
3
4
5
6
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)

后面还有一句 defineComputed 我们也注意一下。然后下面的代码也是用来报错的,暂时不管。服务端SSR 这一块我们也不考虑。

整的来说,这一部分代码我们可以简化一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function initComputed (vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)

for (const key in computed) {
const userDef = computed[key]
const getter = userDef

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

defineComputed(vm, key, userDef)
}
}

现在就很清晰了。所以接下来的核心便是 WatcherdefineComputed

定义计算属性

还是 src/core/instance/state.js 这个文件,我们往下翻到下面这段代码:

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
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
const shouldCache = !isServerRendering()
if (typeof userDef === 'function') {
sharedPropertyDefinition.get = shouldCache
? createComputedGetter(key)
: createGetterInvoker(userDef)
sharedPropertyDefinition.set = noop
} else {
sharedPropertyDefinition.get = userDef.get
? shouldCache && userDef.cache !== false
? createComputedGetter(key)
: createGetterInvoker(userDef.get)
: noop
sharedPropertyDefinition.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)
}

首先我们就说了,不考虑服务端渲染,所以默认 shouldCachetrue,然后我们假设用户定义就是一个函数(定义是对象的话也是同理的)。

下面又是判断环境的,直接不考虑。所以简化后代码是这样的:

1
2
3
4
5
6
7
8
9
export function defineComputed (
target: any,
key: string,
userDef: Object | Function
) {
sharedPropertyDefinition.get = createComputedGetter(key)
sharedPropertyDefinition.set = noop
Object.defineProperty(target, key, sharedPropertyDefinition)
}

对,就三句话!

接下来我们来看 createComputedGetter 这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}
}

我们继续删代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function createComputedGetter (key) {
return function computedGetter () {
// 注:根据函数调用的逻辑,这里的 this 应该是 vm
const watcher = this._computedWatchers[key]
if (watcher.dirty) {
watcher.evaluate()
}
if (Dep.target) {
watcher.depend()
}
return watcher.value
}
}

根据函数调用的逻辑,这里的 this 应该是 vm

这里的 watch 就是 initComputed函数里创建的 watcher 对象,而且每个计算属性都有一个属于自己的 Watcher 对象。

所以我们需要关注几个东西:

  • Watcherwatcher.dirtywatcher.evaluatewatcher.depend
  • Dep.target

接下来我们先康康数据是如何定义的。

数据的 getter 和 setter

我们翻到源码的 src/core/observer/index.js,找到 defineReactive 这个函数(约第135行):

注:data 属性的解析流程可以参考 src/core/instance/state.js 里面的 initData 相关代码,最终可发现它的调用到了 defineReactive 这里

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
64
import Dep from './dep'
/**
* Define a reactive property on an Object.
*/
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()

const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}

// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}

let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const 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) {
const 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()
}
})
}

可能大家并不知道这个函数是干嘛的,我们先来看注释,可以大概看出这是用来在对象上定义一个响应式的数据的。

第一行代码定义了一个 dep 实例,这又是之前的 Dep 对象

中间一堆代码看上去是做一些判断,先跳过。

接下来是 Object.defineProperty,熟悉响应式原理的朋友应该清楚这是Vue2的响应式原理的核心。我们就从这里开始看吧。

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
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const 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) {
const 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()
}
})

getter 那里可能看起来简单一些,大概是先调用了 getter,然后调用了 dep.dependchildOb 可能看起来是一个监听子对象的,暂时也忽略吧。

setter 这里又出现了一个 dep.notify,看来 dep 对象很重要啊。

深入 Watcher

createComputedGetter 这个函数里面我们看到里面一个函数返回的是 watcher.value,然后找到当前目录下的 watcher.js,发现它的构造函数里面有一句:

1
2
3
this.value = this.lazy
? undefined
: this.get()

这说明,watcher.value 这个值大概率是 get 函数的返回值。

但具体是怎么调用和刷新的,咱们先不深究。我们看到 get 函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

我们还是把几行重要的代码留着:

1
2
3
4
5
6
7
8
9
get () {
pushTarget(this)
let value
const vm = this.vm
value = this.getter.call(vm, vm)
popTarget()
this.cleanupDeps()
return value
}

可以看到它和 dep 这个库有着很强的交互。

深究 dep

我们来找到当前目录下的 dep.js

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
64
import type Watcher from './watcher'
import { remove } from '../util/index'
import config from '../config'

let uid = 0

/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;

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

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

removeSub (sub: Watcher) {
remove(this.subs, sub)
}

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

notify () {
// stabilize the subscriber list first
const 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((a, b) => a.id - b.id)
}
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
const targetStack = []

export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}

export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}

这段代码看起来很简洁,但缺失这套机制的核心。

首先看 pushTargetpopTarget。很明显,这使用了一个栈,表示把当前的 watcher 推入和弹出 Dep.target

再来看上面的get的代码,我们不妨可以看出其整体流程了:

graph LR
PUSH[将当前 watcher 入栈]-->CALL[调用 getter]-->POP[将当前 watcher 出栈]

这也可以看出,Dep.target 表示了当前将要执行的 getter 所在的 watcher 对象。对于计算属性来说,就是在执行计算属性定义的那个函数。在执行前入栈,执行后出栈。

接下来我们回顾 defineReactive 函数,它先创建了一个 Dep(每个 data 都有一个唯一的 Dep 对象),然后在 getter 里面调用 dep.depend 函数;在 setter 里面则调用 dep.notify

先看 depend 函数,就是在目标 watcher 上注册自己。再来看watcher 中的 addDep 这个函数:

1
2
3
4
5
6
7
8
9
10
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

我们大概知道这个函数主要是在DepWatcher双方各保留一份对方的引用。

这意味着,在计算属性函数调用的过程中,被访问到的所有 data 都会在内部的 getter 执行 depend 函数,使得计算属性所在的 watcher 知道它依赖了哪些数据属性。

我们再看 notify 代码:

1
2
3
4
5
6
7
notify () {
const subs = this.subs.slice()
// ...
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}

这很明朗了,就是通知 watcher 去更新数据。而且 notify 是通过数据的 set 调用的,这意味着当 data 中的数据修改时,就会触发与之相关联的计算属性进行更新。

计算属性的缓存

defineComputed 函数中我们可以看到这一行:

1
2
3
4
5
6
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)

我们找到 computedWatcherOptions,发现:

1
const computedWatcherOptions = { lazy: true }

然后我们来看 watcherupdate 函数:

1
2
3
4
5
6
7
8
9
10
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

由于这里 lazytrue,所以,它只做了一件事情:把 this.dirty 设置为 true

我们再来看 createComputedGetter这个函数的这段代码:

1
2
3
4
5
6
7
8
9
10
if (watcher) {
if (watcher.dirty) {
watcher.evaluate()
}
// 和普通的 data 是一样的
if (Dep.target) {
watcher.depend()
}
return watcher.value
}

结果发现,如果 watcher.dirtytrue,则执行 evaluate 函数,我们来看一下:

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

这个函数就是重新执行 get 函数,然后把 dirty 设置为 false

这里的逻辑就清晰了。如果 watcher.dirtytrue,那么重新执行 getter,生成新的值;否则,使用原来的值。

小结

看完了这么多的代码,我们来小结一下。

计算属性 getter 调用

  1. 把计算属性对应的 watcher 加入依赖目标栈中,同时设置 Dep.target
  2. 执行计算属性的 getter
  3. 执行过程中,任何被访问到的 data ,其对应的 dep 和 计算属性 watcher 都会形成响应的依赖

这整个过程称为 依赖收集。而 Dep.target 表示了被访问的 data 的访问源。

被依赖数据的更新

这个就很简单了,它会通过自己的 dep 对象通知所有的 watcher 进行更新。但只有下次访问这个计算属性的时候,才会执行计算属性里面定义的函数。

这个过程称为 依赖更新

设计技巧

  1. 栈的使用:收集了计算属性调用过程中所有 watcher
  2. 观察者模式:每一个 Dep 对象通过 notify 函数对所有的 watcher 调用 update。其中被观察者是被依赖数据,观察者是计算属性。
  3. 依赖收集技术:如果一个函数调用内没有异步代码,那么它调用的任何外部的东西都是可以跟踪的。

实现一个简单的依赖收集

这里面实现了一个简单的依赖收集,在函数调用过程中,收集其访问到的数据。

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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
class Watcher {
deps = []

addDep (dep) {
this.deps.push(dep)
dep.addSub(this)
}
}

class Dep {
static target = null
static targets = []
subs = []

addSub (watcher) {
this.subs.push(watcher)
}

removeSub (watcher) {
this.subs = this.subs.filter(watcher)
}

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

Dep.push = (watcher) => {
Dep.targets.push(watcher)
Dep.target = watcher
}

Dep.pop = (watcher) => {
Dep.targets.pop(watcher)
Dep.target = Dep.targets[Dep.targets.length - 1]
}

// 初始化数据
function initData(origin) {
let inited = {
__data: origin
}
for (const key in origin) {
let dep = new Dep();
dep.__key = key // 方便查看
Object.defineProperty(inited, key, {
get () {
dep.depend()
return inited.__data[key]
},
set (val) {
inited.__data[key] = val
}
})
}
return inited
}

/**
* 运行函数
*/
function run(fn, ...args)
{
if (!fn.watcher) fn.watcher = new Watcher()
Dep.push(fn.watcher)
const result = fn(...args)
Dep.pop(fn.watcher)

// 打印收集的依赖
console.log(fn.watcher.deps)
return result
}

let data = initData({
firstName : "123456",
lastName: "hello",
id: 1,
})

function test () {
return data.firstName + data.lastName
}

run(test)

  • 标题: Vue2计算属性的实现原理
  • 作者: ObjectKaz
  • 创建于: 2021-07-23 16:02:41
  • 更新于: 2021-08-07 07:14:50
  • 链接: https://www.objectkaz.cn/432d06cc8cf6.html
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。