侦听属性的初始化也是发生在 Vue
的实例初始化阶段的 initState
函数中,在 computed
初始化之后,执行了:
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
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
对象做遍历,拿到每一个 handler
,因为 Vue
是支持 watch
的同一个 key 对应多个 handler
,所以如果 handler
是一个数组,则遍历这个数组,调用 createWatcher
方法,否则直接调用 createWatcher
:
function createWatcher (
vm: Component,
keyOrFn: string | Function,
handler: any,
options?: Object
) {
if (isPlainObject(handler)) {
options = handler
handler = handler.handler
}
if (typeof handler === 'string') {
handler = vm[handler]
}
return vm.$watch(keyOrFn, handler, options)
}
这里的逻辑也很简单,首先对 hanlder
的类型做判断,拿到它最终的回调函数,最后调用 vm.$watch(keyOrFn, handler, options)
函数,$watch
是 Vue
原型上的方法,它是在执行 stateMixin ()
,目录src/core/instance/state.js
的时候定义的:
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) {
cb.call(vm, watcher.value)
}
return function unwatchFn () {
watcher.teardown()
}
}
也就是说,侦听属性 watch
最终会调用 $watch
方法,这个方法首先判断 cb
如果是一个对象,则调用 createWatcher
方法,这是因为 $watch
方法是用户可以直接调用的,它可以传递一个对象,也可以传递函数。接着执行const watcher = new Watcher(vm, expOrFn, cb, options)
实例化了一个 watcher
,这里需要注意一点这是一个 user watcher
,因为 options.user = true
。通过实例化 watcher 的方式,一旦我们 watch
的数据发送变化,它最终会执行 watcher
的 run
方法,执行回调函数 cb
,并且如果我们设置了immediate
为 true
,则直接会执行回调函数 cb
。最后返回了一个 unwatchFn
方法,它会调用 teardown
方法去移除这个 watcher
。
所以本质上侦听属性也是基于 Watcher
实现的,它是一个 user watcher
。其实 Watcher
支持了不同的类型,下面我们梳理一下它有哪些类型以及它们的作用。
Watcher
的构造函数对 options
做的了处理,代码如下:
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
} else {
this.deep = this.user = this.lazy = this.sync = false
}
所以 watcher
总共有 4 种类型,我们来一一分析它们,看看不同的类型执行的逻辑有哪些差别。
deep watcher
通常,如果我们想对一下对象做深度观测的时候,需要设置这个属性为 true
,考虑到这种情况:
var vm = new Vue({
data() {
a: {
b: 1
}
},
watch: {
a: {
handler(newVal) {
console.log(newVal)
}
}
}
})
vm.a.b = 2
这个时候是不会 log
任何数据的,因为我们是 watch
了 a
对象,只触发了 a
的 getter
,并没有触发 a.b
的 getter
,所以并没有订阅它的变化,导致我们对 vm.a.b = 2
赋值的时候,虽然触发了 setter
,但没有可通知的对象,所以也并不会触发 watch
的回调函数了。
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch: {
a: {
deep: true,
handler(newVal) {
console.log(newVal)
}
}
}
这样就创建了一个 deep watcher
了,在 watcher
执行 get
求值的过程中有一段逻辑:
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value) // 触发它所有子项的 `getter`
}
在对 watch
的表达式或者函数求值后,会调用 traverse
函数,它的定义在 src/core/observer/traverse.js
中:
/**
* Recursively traverse an object to evoke all converted 递归遍历一个对象以调用所有转换的
* getters, so that every nested property inside the object getter,以便对象中的每个嵌套属性
* is collected as a "deep" dependency. 作为“深层”依赖关系收集
*/
const seenObjects = new Set()
function traverse (val: any) {
seenObjects.clear()
_traverse(val, seenObjects)
}
function _traverse (val: any, seen: ISet) {
let i, keys
const isA = Array.isArray(val)
if ((!isA && !isObject(val)) || !Object.isExtensible(val)) {
return
}
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)
}
}
traverse
的逻辑也很简单,它实际上就是对一个对象做深层递归遍历,因为遍历过程中就是对一个子对象的访问,会触发它们的 getter
过程,这样就可以收集到依赖,也就是订阅它们变化的 watcher
,这个函数实现还有一个小的优化,遍历过程中会把子响应式对象通过它们的 dep id
记录到 seenObjects
,避免以后重复访问。
那么在执行了 traverse
后,我们再对 watch
的对象内部任何一个值做修改,也会调用 watcher
的回调函数了。
对 deep watcher
的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deep
为 true
,但是因为设置了 deep
后会执行 traverse
函数,会有一定的性能开销,所以一定要根据应用场景权衡是否要开启这个配置。
user watcher
前面我们分析过,通过 vm.$watch
创建的 watcher
是一个 user watcher
,其实它的功能很简单,在对 watcher
求值以及在执行回调函数的时候,会处理一下错误,如下:
get() {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
},
set() {
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)
}
}
handleError
在 Vue
中是一个错误捕获并且暴露给用户的一个利器。
computed watcher
computed watcher
几乎就是为计算属性量身定制的,之前文章中有对它做了详细的分析,这里不再赘述了,详细点这里。
sync watcher
在我们之前对 setter
的分析过程知道,当响应式数据发送变化后,触发了 watcher.update()
,只是把这个 watcher
推送到一个队列中,在 nextTick
后才会真正执行 watcher
的回调函数。而一旦我们设置了 sync
,就可以在当前 Tick
中同步执行 watcher
的回调函数。
/**
* Subscriber interface.
* Will be called when a dependency changes. //
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
只有当我们需要 watch
的值的变化到执行 watcher
的回调函数是一个同步过程的时候才会去设置该属性为 true
。