Vue watcher 分类

前言

知道 Vue 响应式原理的都知道,每个组件都对应有一个自己的 watcher 实例,但是除了组件对应的 watcher 实例,computed 和 watch 也都分别对应有自己的 watcher 实例,所以下面我们就针对这三种 watcher 做一下研究,知道了底层实现,也能更好的让我们在使用中做出更好的抉择。

Watcher 的初始化发生在 Vue 实例初始化阶段的 initState 函数中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// ..
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm)
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
// ..
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm)
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}

这里先创建了 computed 和 watch 的 watcher 实例,组件的 watcher 实例是在执行 vm.$mount 后创建的,所以这三种 watcher 执行顺序为 computed watcher => user watcher => render watcher。

一、computed watcher

1、数据劫持

计算属性的初始化是发生在 Vue 实例初始化阶段的 initState 函数中。先遍历 computed 对象,为每一个计算属性创建一个 computed watcher。然后判断如果该计算属性不是 vm 的属性,则调用 defineComputed(vm, key, userDef),否则判断计算属性是否已经被 dataprop 占用,如果占用则在开发环境报相应警告。

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 // computedWatcherOptions = { computed: true }
)
}

// 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)
}
}
}
}

利用 Object.defineProperty 给每一个计算属性添加 getter 和 setter。setter 通常是计算属性是一个对象,并且拥有 set 方法的时候才有,否则是一个空函数。在平时的开发场景中,计算属性有 setter 的情况比较少。

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

2、依赖收集

当调用 render 函数访问到计算属性的时候,就触发了计算属性的 getter 进行求值,将 dirty 设为 false,用于缓存getter 会触发依赖的属性去收集 render watcher 和此时的 computed watcher

3、依赖更新

一旦我们对计算属性依赖的数据做修改,则会触发 setter ,通知所有订阅它变化的 watcher 更新,包括 render watchercomputed watcher 。对于 computed watcher 就是将 dirty 设为 true。对于 render watcher 会执行 Watcher 实例的 run 方法重新执行组件的render函数。这样在渲染组件的过程中又会触发计算属性,然后继续对计算属性进行求值,使得依赖的属性继续收集 computed watcher

二、user watcher

1、数据劫持

侦听属性的初始化也是发生在 Vue 的实例初始化阶段的 initState 函数中。遍历 watch 对象,拿到每一个 handler,因为 Vue 是支持 watch 的同一个 key 对应多个 handler,所以如果 handler 是一个数组,则遍历这个数组,每一项调用 createWatcher 方法,否则直接调用 createWatcher。然后创建一个 user watcher

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)
}
}
}
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)
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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) {
const info = `callback for immediate watcher "${watcher.expression}"`
pushTarget()
invokeWithErrorHandling(cb, vm, [watcher.value], vm, info)
popTarget()
}
return function unwatchFn () {
watcher.teardown()
}
}

2、依赖收集

当初始化试图或更新试图访问属性的 getter 方法时,会触发属性去收集 render watcher 和对应的的 user watcher

3、依赖更新

一旦我们对数据做修改,则会触发 setter ,通知所有订阅它变化的 watcher 更新,包括 render watcheruser watcher

4、其他特性

4.1、deep

如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true。

这个时候是不会 log 任何数据的,因为我们是 watch 了 a 对象,只触发了 a 的 getter,并没有触发 a.b 的 getter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var vm = new Vue({
data() {
a: {
b: 1
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
}
}
}
})
vm.a.b = 2

而我们只需要对代码做稍稍修改,就可以观测到这个变化了

1
2
3
4
5
6
7
8
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}

这样在 watcher 执行 get 求值的过程中有一段逻辑,traverse 实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter 过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher。

1
2
3
4
5
6
7
get() {
let value = this.getter.call(vm, vm)
// ...
if (this.deep) {
traverse(value)
}
}

三、render watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
export function mountComponent (
vm: Component,
el: ?Element,
hydrating?: boolean
): Component {
// ...
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)
// ...
}

详情见 Vue 响应式原理

四、问题

1、computed 和 watch 的区别

  • computed: 依赖的属性值发生变化才会重新计算 computed 的值;
  • watch: 监听的数据发生变化就会执行回调进行后续操作;
微信打赏