Vue源码-计算和监听属性

在Vue的每个组件都有一个渲染watcher,它会被模版用到的数据作为依赖收集,在状态发生变化时,会通知该watcher,从而使组件重新执行renderpatch,最后渲染最新的视图。组件中除了渲染watcher之外,还有计算属性computed和监听属性watch,它们在Vue内部也是watcher的一种。

计算属性

Vue的计算属性常用在模版,并且它的值由响应对象的属性计算而来。来看下面例子:

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
// App.vue
<template>
<div class="app">
<p>{{ fullName }}</p>
<button @click="handleClick">click</button>
</div>
</template>

<script>
export default {
name: 'app',
data() {
return {
firstName: 'wozien',
secondName: 'zhang'
};
},
computed: {
fullName() {
return this.firstName + '-' + this.secondName;
}
},
methods: {
handleClick() {
this.firstName = 'wozien1';
}
}
};
</script>
`

上面的例子其实可以用方法实现,但是计算属性和方法的区别就是具有缓存的机制,而方法在每次render的时候都会重新执行一遍。现在我们通过源码来看看Vue是怎么设计计算属性的。

计算属性的初始化是在src/core/instance/state.js中的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
const computedWatcherOptions = { lazy: true }

function initComputed(vm: Component, computed: Object) {
const watchers = vm._computedWatchers = Object.create(null)
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) {
// 新建一个计算watcher
watchers[key] = new Watcher(
vm,
getter || noop,
noop,
computedWatcherOptions
)
}

// 组件的computed会在新建组件构造器是挂载到原型对象上
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)
}
}
}
}

这个方法先创建一个空对象vm._computedWatchers存储实例的所有计算watcher,然后遍历计算属性,把计算函数作为getter新建对应的计算watcher。这里和渲染watcher的一个区别就是它有一个配置项{ lazy: true }, 在构造函数会走下面逻辑:

1
2
3
4
5
6
this.dirty = this.lazy 

// ...
this.value = this.lazy
? undefined
: this.get()

很显然,新建一个计算watcher不会马上执行get方法。接下来,判断如果计算属性的key不存在实例上,会调用defineComputed方法定义计算属性,否则判断是否和data或者props重名。

对于组件的计算属性,调用defineComputed方法是在新建组件构造器是定义的,它把计算属性挂载到构造器的原型对象。这样做的目的是防止每次新建组件实例都去重新定义一遍。它定义在Vue.extend()中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Vue.extend = function (extendOptions: Object): Function {
// ...
if (Sub.options.computed) {
initComputed(Sub)
}

// ...
}

function initComputed (Comp) {
const computed = Comp.options.computed
for (const key in computed) {
defineComputed(Comp.prototype, key, computed[key])
}
}

现在来看下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
// 用Object.defineProperty定义计算属性
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)
}

这个方法主要就是把计算属性定义在target上,并且通过createComputedGetter方法设置getter,对于setter用户可以自定义否则为空函数noop。现在我们看下createComputedGetter是怎么定义的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function createComputedGetter (key) {
return function computedGetter () {
const watcher = this._computedWatchers && this._computedWatchers[key]
if (watcher) {
if (watcher.dirty) {
// 计算属性获取最新值
watcher.evaluate()
}
if (Dep.target) {
// 让计算属性的关联的属性收集渲染watcher
watcher.depend()
}
return watcher.value
}
}
}

这个方法返回了计算属性的getter函数。当我们正在执行渲染watcherget方法时,对于组件模版访问到的计算属性,就会触发这个getter

首先,通过实例上_computedWatchers获取到计算watcher。如果watcher.dirtytrue,就是执行evaluate方法:

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

所以,计算watcher的求值会在模版render的时候去调用,然后把this.dirty设置为false,所以下面再次访问计算属性会直接返回之前保存的值。那么什么时候会还原成true呢?那当然是计算属性依赖的状态数据发生改变时,接下来我们会分析到。

另外一个注意的是,在执行计算watcherget方法时,也就是执行计算函数。比如我们例子中的:

1
2
3
fullName() {
return this.firstName + '-' + this.secondName;
}

在执行这个函数时,用到的firstName和secondName状态会触发getter,然后把fullName的计算watcher作为依赖进行收集。在计算属性求完值后,会执行下面逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
if (Dep.target) {
// 让计算属性的关联的属性收集渲染watcher
watcher.depend()
}

// watcher.js
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}

这端代码的意思就是让计算属性依赖的状态收集当前的Dep.target。很明显,当前的Dep.target就是组件的渲染watcher。所以我们的例子的firstName和secondName都有两个依赖,分别是计算watcher和渲染watcher。这样我们的组件就渲染结束,接下来就看看当计算属性的依赖状态发生改变时Vue的处理。

比如我们例子点击后会触发this.firstName = 'wozien1',然后会执行firstName状态的setter通知依赖,调用依赖的update方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
// watcher.js
update () {
/* istanbul ignore else */
if (this.lazy) {
// 计算属性watcher的更新把dirty设为true
// 在执行渲染watcher的时候获取最新的值
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}

对于第一个依赖是计算watcher,它的lazytrue,所以只是简单的把dirty重新设置为true就结束了。然后到了渲染watcher,它会执行queueWatcher(this)在下个tick中执行run方法:

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
run () {
if (this.active) {
// 获取新值
const value = this.get()
if (
value !== this.value ||
// Deep watchers and watchers on Object/Arrays should fire even
// when the value is the same, because the value may
// have mutated.
isObject(value) ||
this.deep
) {
// set new value
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}

执行run方法组件就会重新render,这个时候计算属性由于dirty已经重置为true,所以会执行evaluate获取最新的值来渲染视图。因此,我们的状态改变导致计算属性变化进而更新视图的流程就结束了。你会发现,如果我们的状态设置成和之前一样的值,这个时候不会触发状态setter,也就是计算watcherdirty还是为false,所以计算属性还是缓存了之前的值。

说白了就是,如果模版中用到了计算属性,那么计算属性依赖的状态的改变必然会引起模版的变化,所以把渲染watcher收集进状态的dep即可。然后在触发渲染watcher要把计算watcher的缓存标记dirty设置为true,获取计算属性最新值。

监听属性

对于监听属性的入口是定义src/core/instance/state.js中的initWatch方法:

1
2
3
4
5
6
7
8
9
10
11
12
function initWatch (vm: Component, watch: Object) {
for (const key in watch) {
const handler = watch[key]
if (Array.isArray(handler)) {
for (let i = 0; i < handler.length; i++) {
createWatcher(vm, key, handler[i])
}
} else {
createWatcher(vm, key, handler)
}
}
}

这个方法主要遍历实例的watch配置调用createWatcher方法。因为Vue允许我们为一个watch添加多个handler,所以要处理handler是数组的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function createWatcher (
vm: Component,
expOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(expOrFn, handler, options)
}

首先处理下watch的配置是一个对象的情况,获取对象的handler最后调用vm.$watch方法。这个方法是stateMixins挂载到原型上的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
Vue.prototype.$watch = function (
expOrFn: string | Function,
cb: any,
options?: Object
): Function {
const vm: Component = this
if (isPlainObject(cb)) {
return createWatcher(vm, expOrFn, cb, options)
}
options = options || {}
options.user = true
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
try {
cb.call(vm, watcher.value)
} catch (error) {
handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`)
}
}
return function unwatchFn () {
watcher.teardown()
}
}

这个方法为实例的监听属性新建一个watcher。如果expOrFn是一个函数的话,它会作为watchergetter。否则会执行下面的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// watcher.js
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
// 字符串情况
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}

parsePath方法是获取字符串形式的对象属性,并且返回一个获取的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const bailRE = new RegExp(`[^${unicodeRegExp.source}.$_\\d]`)
export function parsePath (path: string): any {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function (obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
return obj
}
}

这个方法就是把字符串按.切割,然后循环就可以访问到最终监听的属性。这样两种方式的监听属性的getter都可以拿到,在执行getter后就会把该watcher作为依赖被监听的属性收集。要注意的是,如果监听属性是一个函数,则这个函数里面访问的属性都会收集这个watcher。然后在状态发生改变时,就会通知这个watcher执行update,最后在下个tick中执行对应的回调函数。

接下来对于配置了immediatetrue的属性,马上执行回调函数。最后返回一个可以卸载这个watcher的函数,卸载watcher主要是调用teardown方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}

这个方法首先把自己从实例中移除,然后循环从订阅的Dep中移除。因为我们的watcher回调函数是在下一tick执行的,也就是异步执行的。如果要求回调函数在当前的执行栈中执行,可以设置监听属性的synctrue,然后update方法最直接调用run方法,而不是丢进watcher队列:

1
2
3
4
5
6
7
8
9
10
11
12
13
update () {
/* istanbul ignore else */
if (this.lazy) {
// 计算属性watcher的更新把dirty设为true
// 在执行渲染watcher的时候获取最新的值
this.dirty = true
} else if (this.sync) {
// 同步执行
this.run()
} else {
queueWatcher(this)
}
}

另外一种情况,如果我们监听的是一个对象,并且我们想要对象的子属性发生变化也要触发回调函数。我们可以设置deeptrue

1
2
3
4
5
6
watch: {
obj: {
handler: function(){},
deep: true
}
}

现在来看下Vue对deep处理。在执行watcherget时候有这样一段逻辑:

1
2
3
if (this.deep) {
traverse(value)
}

traverse方法主要是循环访问value的属性,触发属性的getter,因为当前Dep.target指向这个监听watcher,所以value的属性也会收集这个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
const seenObjects = new Set()

export function traverse (val: any) {
_traverse(val, seenObjects)
seenObjects.clear()
}

function _traverse (val: any, seen: SimpleSet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || Object.isFrozen(val) || val instanceof VNode) {
return
}
// 对响应式数据deep才有效
if (val.__ob__) {
const depId = val.__ob__.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
i = val.length
while (i--) _traverse(val[i], seen)
} else {
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
}
}

总结

现在,我们对Vue的所有类型的watcher都分析完了,其实它们的背后逻辑都是一样的。就是把watcher自身作为依赖收集进状态的Dep,在状态发生改变时,执行回调函数或重新渲染组件。