上一篇基本上搞懂了怎么定义响应式对象的,这里研究一下怎么触发侦测的
api/watch.ts watch 之前比较疑惑的是,为什么 watch 的第一个参数需要传递一个 getter 或者 ref,而不能传递 reactive.prop
。 其实这里是调用了 Vue.$watch 方法
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 const stop = vm.$watch(getter, callback, { immediate : options.immediate, deep : deep, sync : isSync, }); 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 { cb.call(vm, watcher.value) } catch (error) { handleError(error, vm, `callback for immediate watcher "${watcher.expression} "` ) } } return function unwatchFn ( ) { watcher.teardown() } } } export default class Watcher { constructor ( vm: Component, expOrFn: string | Function , cb: Function , options?: ?Object , isRenderWatcher?: boolean ) { if (typeof expOrFn === "function" ) { this .getter = expOrFn; } else { this .getter = parsePath(expOrFn); if (!this .getter) { this .getter = noop; } } this .value = this .lazy ? undefined : this .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 { if (this .deep) { traverse(value); } popTarget(); this .cleanupDeps(); } return value; } }
可以看到 $watch 要求传入 getter 或者一个路径字符串,这个 getter 是要在进行依赖收集的时候被调用的 composition-api 中的 watch 使用了 vue2 自身的 watch 机制
至于 watch 的响应式原理,大致如下吧
通过看源码也搞清了一个点,这里的 childObj 和深度监听一点关系都没有
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 export function defineReactive ( obj: Object , key: string , val: any , customSetter?: ?Function , shallow?: boolean ) { let childOb = !shallow && observe(val); Object .defineProperty(obj, key, { enumerable : true , configurable : true , get : function reactiveGetter ( ) { const value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array .isArray(value)) { dependArray(value); } } } return value; },
它的作用是:每个 Observer 对象都有一个 dep 属性,如果使用 Vue.$set 给对象动态添加新属性,会给赋予该属性响应式并调用该对象的 dep 的 notify
看下面的例子应该就可以完全理解了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export default { data ( ) { return { ttt : { a : 1 , }, }; }, mounted ( ) { setTimeout (() => { this .ttt.a = 3 ; this .$set(this .ttt, "a" , 5 ); this .$set(this .ttt, "b" , 5 ); this .ttt = {}; }, 1000 ); }, watch : { ttt (val ) { console .log("watch" , val); }, }, };
那么看一下深度监听是怎么实现的
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 class Watcher { 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 { if (this .deep) { traverse(value); } popTarget(); this .cleanupDeps(); } return value; } } 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__) { 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); } }
就是递归遍历,深度访问对象所有的值,触发对象所有值的依赖收集。 还有一个点,Vue.$watch 返回了一个方法用于停止监听,看下它是怎么实现的
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 43 44 45 46 47 48 49 Vue.prototype.$watch = function ( expOrFn: string | Function , cb: any, options?: Object ): Function { return function unwatchFn ( ) { watcher.teardown(); }; }; class Watcher { 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 ; } } } class Dep { removeSub (sub: Watcher ) { remove(this .subs, sub); } } export function remove (arr: Array <any>, item: any ): Array <any > | void { if (arr.length) { const index = arr.indexOf(item); if (index > -1 ) { return arr.splice(index, 1 ); } } }
就是把 watcher 从所有依赖的属性的 subs 中删除
computed 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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 export function computed <T >( options: Option<T>["get" ] | Option<T> ): ComputedRef <T > | WritableComputedRef <T > { const vm = getCurrentInstance()?.proxy; let get: Option<T>["get" ], set : Option<T>["set" ] | undefined ; if (typeof options === "function" ) { get = options; } else { get = options.get; set = options.set; } let computedSetter; let computedGetter; if (vm && !vm.$isServer) { const { Watcher, Dep } = getVueInternalClasses(); let watcher: any ; computedGetter = () => { if (!watcher) { watcher = new Watcher(vm, get, noopFn, { lazy : true }); } if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); } return watcher.value; }; computedSetter = (v: T ) => { if (set) { set(v); } }; } else { const computedHost = defineComponentInstance(getVueConstructor(), { computed : { $$state : { get, set, }, }, }); vm && vm.$on("hook:destroyed" , () => computedHost.$destroy()); computedGetter = () => (computedHost as any ).$$state; computedSetter = (v: T ) => { (computedHost as any ).$$state = v; }; } return createRef<T>( { get : computedGetter, set : computedSetter, }, !set ); }
通过 createRef 传递 get 和 set 创建了 Ref。 computed 是惰性的(初始时 lazy 和 dirty 都为 true),只有在被求值的时候才会计算,然后进行相应的依赖收集
1 2 3 4 5 6 if (watcher.dirty) { watcher.evaluate(); } if (Dep.target) { watcher.depend(); }
这里有点难理解的,其实是对应了这种情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 export default { name : "App" , data ( ) { return { t : 5 , }; }, mounted ( ) { setTimeout (() => { this .t = 10 ; console .log(this .tttt); }, 1000 ); }, computed : { tt ( ) { return this .t; }, ttt ( ) { return this .tt; }, }, };
当 computed 依赖于另外的 computed 时,访问 t 的 getter 只会给 tt 的 watcher 添加订阅,但是我们想要的给 tt 和 ttt 的 watcher 都添加订阅 Vue 对于 Dep.target 使用了一个栈去管理,而这种嵌套的 computed 的访问也是栈式的。 访问 ttt 的 getter 时,执行 ttt 的 watcher 的 evaluate(),ttt 的 watcher 入栈,然后嵌套访问 tt 的 getter,然后执行 tt 的 watcher 的 evaluate(),tt 的 watcher 入栈。tt 这一层的 evaluate() 执行完毕后 tt 的 watcher 退栈,这时 Dep.target 是指向 ttt.watcher 的,执行 watcher.depend(),就可以给 ttt 的 watcher 添加订阅了。