vue2.0源码解读|vue2.0源码解读 - 监听属性 watch

侦听属性的初始化也是发生在 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) 函数,$watchVue 原型上的方法,它是在执行 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 的数据发送变化,它最终会执行 watcherrun方法,执行回调函数 cb,并且如果我们设置了immediatetrue,则直接会执行回调函数 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 任何数据的,因为我们是 watcha 对象,只触发了 agetter,并没有触发 a.bgetter,所以并没有订阅它的变化,导致我们对 vm.a.b = 2 赋值的时候,虽然触发了 setter,但没有可通知的对象,所以也并不会触发 watch 的回调函数了。
而我们只需要对代码做稍稍修改,就可以观测到这个变化了
watch: { a: { deep: true, handler(newVal) { console.log(newVal) } } }

【vue2.0源码解读|vue2.0源码解读 - 监听属性 watch】这样就创建了一个 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 的理解非常重要,今后工作中如果大家观测了一个复杂对象,并且会改变对象内部深层某个值的时候也希望触发回调,一定要设置 deeptrue,但是因为设置了 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) } }

handleErrorVue 中是一个错误捕获并且暴露给用户的一个利器。
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

    推荐阅读