判断是否需要深度监听,调用 traverse 并将值传入
// 源码位置:/src/core/observer/traverse.js 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 } if (val.__ob__) { // 1 const depId = val.__ob__.dep.id // 2 if (seen.has(depId)) { return } seen.add(depId) } // 3 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) } }depId 是每一个被观察属性都会有的唯一标识
去重,防止相同属性重复执行逻辑
根据数组和对象使用不同的策略,最终目的是递归获取每一项属性,触发它们的“数据劫持get”收集依赖,和 parsePath 的效果是异曲同工
从这里能得出,深度监听利用递归进行监听,肯定会有性能损耗。因为每一项属性都要走一遍依赖收集流程,所以在业务中尽量避免这类操作。
卸载监听这种手段在业务中基本很少用,也不算是重点,属于那种少用但很有用的方法。它作为 watch 的一部分,这里也讲下它的原理。
使用先来看看它的用法:
data(){ return { name: 'jojo' } } mounted() { let unwatchFn = this.$watch('name', () => {}) setTimeout(()=>{ unwatchFn() }, 10000) }使用 $watch 监听数据后,会返回一个对应的卸载监听函数。顾名思义,调用它当然就是不会再监听数据。
原理 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 { // 立即调用 watch cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression}"`) } } return function unwatchFn () { watcher.teardown() } }可以看到返回的 unwatchFn 里实际执行的是 teardown。
teardown () { if (this.active) { if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } }teardown 里的操作也很简单,遍历 deps 调用 removeSub 方法,移除当前 watcher 实例。在下一次属性更新时,也不会通知 watcher 更新了。deps 存储的是属性的 dep(依赖收集器)。
奇怪的地方在看源码时,我发现 watch 有个奇怪的地方,导致它的用法是可以这样的:
watch:{ name:{ handler: { handler: { handler: { handler: { handler: { handler: { handler: ()=>{console.log(123)}, immediate: true } } } } } } } }一般 handler 是传递一个函数作为回调,但是对于对象类型,内部会进行递归去获取,直到值为函数。所以你可以无限套娃传对象。
递归的点在 $watch 中的这段代码:
if (isPlainObject(cb)) { return createWatcher(vm, expOrFn, cb, options) }如果你知道这段代码的实际应用场景麻烦告诉我一下,嘿嘿~
总结watch 监听实现利用遍历获取属性,触发“数据劫持get”逐个收集依赖,这样做的好处是其上级的属性发生修改也能执行回调。
与 data 和 computed 不同,watch 收集依赖的流程是发生在页面渲染之前,而前两者是在页面渲染时进行取值才会收集依赖。
在面试时,如果被问到 computed 和 watch 的异同,我们可以从下面这些点进行回答:
一是 computed 要依赖 data 上的属性变化返回一个值,watch 则是观察数据触发回调;
二是 computed 和 watch 依赖收集的发生点不同;