手摸手带你理解Vue的Watch原理 (2)

$watch:

// 源码位置:/src/core/instance/state.js export function stateMixin (Vue: Class<Component>) { 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) } // 1 options = options || {} options.user = true // 2 const watcher = new Watcher(vm, expOrFn, cb, options) // 3 if (options.immediate) { try { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } // 4 return function unwatchFn () { watcher.teardown() } } }

stateMixin 在入口文件就已经调用了,为 Vue 的原型添加 $watch 方法。

所有“用户Watcher”的 options,都会带有 user 标识

创建 watcher,进行依赖收集

immediate 为 true 时,立即调用回调

返回的函数可以用于取消 watch 监听

依赖收集及更新流程

经过上面的流程后,最终会进入 new Watcher 的逻辑,这里面也是依赖收集和更新的触发点。接下来看看这里面会有哪些操作。

依赖收集 // 源码位置:/src/core/observer/watcher.js export default class Watcher { constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean ) { this.vm = vm // options if (options) { this.deep = !!options.deep this.user = !!options.user this.lazy = !!options.lazy this.sync = !!options.sync this.before = options.before } else { this.deep = this.user = this.lazy = this.sync = false } this.cb = cb this.id = ++uid // uid for batching this.active = true this.dirty = this.lazy // for lazy watchers this.deps = [] this.newDeps = [] this.depIds = new Set() this.newDepIds = new Set() // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) } this.value = this.lazy ? undefined : this.get() } }

在 Watcher 构造函数内,对传入的回调和 options 都进行保存,这不是重点。让我们来关注下这段代码:

if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) }

传进来的 expOrFn 是 watch 的键值,因为键值可能是 obj.a.b,需要调用 parsePath 对键值解析,这一步也是依赖收集的关键点。它执行后返回的是一个函数,先不着急 parsePath 做的是什么,先接着流程继续走。

下一步就是调用 get:

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 }

pushTarget 将当前的“用户Watcher”(即当前实例this) 挂到 Dep.target 上,在收集依赖时,找的就是 Dep.target。然后调用 getter 函数,这里就进入 parsePath 的逻辑。

// 源码位置:/src/core/util/lang.js 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 } }

参数 obj 是 vm 实例,segments 是解析后的键值数组,循环去获取每项键值的值,触发它们的“数据劫持get”。接着触发 dep.depend 收集依赖(依赖就是挂在 Dep.target 的 Watcher)。

到这里依赖收集就完成了,从上面我们也得知,每一项键值都会被触发依赖收集,也就是说上面的任何一项键值的值发生改变都会触发 watch 回调。例如:

watch: { 'obj.a.b.c': function(){} }

不仅修改 c 会触发回调,修改 b、a 以及 obj 同样触发回调。这个设计也是很妙,通过简单的循环去为每一项都收集到了依赖。

更新

在更新时首先触发的是“数据劫持set”,调用 dep.notify 通知每一个 watcher 的 update 方法。

update () { if (this.lazy) { dirty置为true this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) } }

接着就走 queueWatcher 进行异步更新,这里先不讲异步更新。只需要知道它最后会调用的是 run 方法。

run () { if (this.active) { const value = this.get() if ( value !== this.value || 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) } } } }

this.get 获取新值,调用 this.cb,将新值旧值传入。

深度监听

深度监听是 watch 监听中一项很重要的配置,它能为我们观察对象中任何一个属性的变化。

目光再拉回到 get 函数,其中有一段代码是这样的:

if (this.deep) { traverse(value) }

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpjpds.html