从测试用例来学习vue3 effect
此次分享主要是 effect
这个 API 的一些功能、option 以及实现原理。
核心内容是 effect 如何做到跟踪 reactive 内的变化的
测试用例
因为 effect.spec.ts 文件中的测试用例 700 多行,所以后面省略了一些,此次不细说,大家自行查看研究。
以下内容来自vue-next/packages/reactivity/tests/effect.spec.ts
-
验证调用次数
it('should run the passed function once (wrapped by a effect)', () => { const fnSpy = jest.fn(() => {}) effect(fnSpy) // 验证这里会立即执行一次函数 expect(fnSpy).toHaveBeenCalledTimes(1) })
-
验证基础响应属性
这块的功能大概就是 effect 的核心了。如何跟踪属性变化并调用回调函数的 是此次分享主要内容it('should observe basic properties', () => { // 定义一个属性 let dummy // 初始化一个响应式的 属性 const counter = reactive({ num: 0 }) // 注意这里回调函数内的操作 effect(() => (dummy = counter.num)) expect(dummy).toBe(0) counter.num = 7 expect(dummy).toBe(7) }) // 多个 reactive 属性 it('should observe multiple properties', () => { let dummy const counter = reactive({ num1: 0, num2: 0 }) effect(() => (dummy = counter.num1 + counter.num1 + counter.num2)) expect(dummy).toBe(0) counter.num1 = counter.num2 = 7 expect(dummy).toBe(21) }) // 触发多个 effect it('should handle multiple effects', () => { let dummy1, dummy2 const counter = reactive({ num: 0 }) effect(() => (dummy1 = counter.num)) effect(() => (dummy2 = counter.num)) expect(dummy1).toBe(0) expect(dummy2).toBe(0) counter.num++ expect(dummy1).toBe(1) expect(dummy2).toBe(1) }) // 嵌套 it('should observe nested properties', () => { let dummy const counter = reactive({ nested: { num: 0 } }) effect(() => (dummy = counter.nested.num)) expect(dummy).toBe(0) counter.nested.num = 8 expect(dummy).toBe(8) }) // 删除属性 it('should observe delete operations', () => { let dummy const obj = reactive({ prop: 'value' }) effect(() => (dummy = obj.prop)) expect(dummy).toBe('value') delete obj.prop expect(dummy).toBe(undefined) }) // 删除后 再添加,验证 has 方法的响应 it('should observe has operations', () => { let dummy const obj = reactive<{ prop: string | number }>({ prop: 'value' }) effect(() => (dummy = 'prop' in obj)) expect(dummy).toBe(true) delete obj.prop expect(dummy).toBe(false) obj.prop = 12 expect(dummy).toBe(true) }) // 原型链上的属性响应测试 it('should observe properties on the prototype chain', () => { let dummy const counter = reactive({ num: 0 }) const parentCounter = reactive({ num: 2 }) // 设置原型对象 Object.setPrototypeOf(counter, parentCounter) effect(() => (dummy = counter.num)) expect(dummy).toBe(0) // 删除自身的 num 属性 delete counter.num expect(dummy).toBe(2) // 测试原型上的 num parentCounter.num = 4 expect(dummy).toBe(4) // 又添加回来了 counter.num = 3 expect(dummy).toBe(3) }) // 和上面大致相同 it('should observe has operations on the prototype chain', () => { let dummy const counter = reactive({ num: 0 }) const parentCounter = reactive({ num: 2 }) Object.setPrototypeOf(counter, parentCounter) effect(() => (dummy = 'num' in counter)) expect(dummy).toBe(true) delete counter.num expect(dummy).toBe(true) delete parentCounter.num expect(dummy).toBe(false) counter.num = 3 expect(dummy).toBe(true) }) // 测试原型上的属性修饰方法 it('should observe inherited property accessors', () => { let dummy, parentDummy, hiddenValue: any const obj = reactive<{ prop?: number }>({}) const parent = reactive({ set prop(value) { hiddenValue = value }, get prop() { return hiddenValue } }) Object.setPrototypeOf(obj, parent) effect(() => (dummy = obj.prop)) effect(() => (parentDummy = parent.prop)) expect(dummy).toBe(undefined) expect(parentDummy).toBe(undefined) obj.prop = 4 expect(dummy).toBe(4) // 这里的 parent.prop === 4 但 parentDummy === undefined // this doesn't work, should it? // expect(parentDummy).toBe(4) parent.prop = 2 expect(dummy).toBe(2) expect(parentDummy).toBe(2) }) // 此次省略 N 多测试用例 //关于这个测试用例比较有意思 it('should observe json methods', () => { let dummy = <Record<string, number>>{} const obj = reactive<Record<string, number>>({}) effect(() => { // 通过 json 转换 dummy = JSON.parse(JSON.stringify(obj)) }) obj.a = 1 // 这里依旧可以跟踪到 expect(dummy.a).toBe(1) })
-
关于一些 option 及其他功能
// options.lazy it('lazy', () => { const obj = reactive({ foo: 1 }) let dummy const runner = effect(() => (dummy = obj.foo), { lazy: true }) expect(dummy).toBe(undefined) // 需要手动执行一次才可以跟踪到 expect(runner()).toBe(1) expect(dummy).toBe(1) obj.foo = 2 expect(dummy).toBe(2) }) // options.scheduler it('scheduler', () => { let runner: any, dummy const scheduler = jest.fn(_runner => { runner = _runner }) const obj = reactive({ foo: 1 }) effect( () => { dummy = obj.foo }, { scheduler } ) expect(scheduler).not.toHaveBeenCalled() // 传入了 scheduler,第一次会默认执行一次 expect(dummy).toBe(1) // should be called on first trigger obj.foo++ expect(scheduler).toHaveBeenCalledTimes(1) // should not run yet expect(dummy).toBe(1) // manually run runner() // should have run expect(dummy).toBe(2) }) // options.onTrack it('events: onTrack', () => { let events: DebuggerEvent[] = [] let dummy const onTrack = jest.fn((e: DebuggerEvent) => { events.push(e) }) const obj = reactive({ foo: 1, bar: 2 }) const runner = effect( () => { // 这里执行 get has 都会调用一次 track dummy = obj.foo dummy = 'bar' in obj dummy = Object.keys(obj) }, { onTrack } ) expect(dummy).toEqual(['foo', 'bar']) // 注意这里 onTrack 被执行了 3 次 expect(onTrack).toHaveBeenCalledTimes(3) expect(events).toEqual([ { effect: runner, target: toRaw(obj), type: OperationTypes.GET, key: 'foo' }, { effect: runner, target: toRaw(obj), type: OperationTypes.HAS, key: 'bar' }, { effect: runner, target: toRaw(obj), type: OperationTypes.ITERATE, key: ITERATE_KEY } ]) }) // options.onTrigger it('events: onTrigger', () => { let events: DebuggerEvent[] = [] let dummy const onTrigger = jest.fn((e: DebuggerEvent) => { events.push(e) }) const obj = reactive({ foo: 1 }) const runner = effect( () => { dummy = obj.foo }, { onTrigger } ) obj.foo++ expect(dummy).toBe(2) expect(onTrigger).toHaveBeenCalledTimes(1) expect(events[0]).toEqual({ effect: runner, target: toRaw(obj), type: OperationTypes.SET, key: 'foo', oldValue: 1, newValue: 2 }) delete obj.foo expect(dummy).toBeUndefined() expect(onTrigger).toHaveBeenCalledTimes(2) expect(events[1]).toEqual({ effect: runner, target: toRaw(obj), type: OperationTypes.DELETE, key: 'foo', oldValue: 2 }) }) // stop 不是在 options 传递的,额外的一个方法 it('stop', () => { let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) obj.prop = 2 expect(dummy).toBe(2) stop(runner) obj.prop = 3 expect(dummy).toBe(2) // stopped effect should still be manually callable runner() expect(dummy).toBe(3) }) // options.onTrigger it('events: onStop', () => { const onStop = jest.fn() const runner = effect(() => {}, { onStop }) stop(runner) expect(onStop).toHaveBeenCalled() }) // stop 后恢复之前的自动跟踪 it('stop: a stopped effect is nested in a normal effect', () => { let dummy const obj = reactive({ prop: 1 }) const runner = effect(() => { dummy = obj.prop }) stop(runner) obj.prop = 2 expect(dummy).toBe(1) // observed value in inner stopped effect // will track outer effect as an dependency // 将 runner 重新放入 effect 中,相当于 dummy = obj.prop 又一次被跟踪 effect(() => { runner() }) expect(dummy).toBe(2) // notify outer effect to run obj.prop = 3 expect(dummy).toBe(3) }) // reactive 被标记了 markNonReactive 不会响应 it('markNonReactive', () => { const obj = reactive({ foo: markNonReactive({ prop: 0 }) }) let dummy effect(() => { dummy = obj.foo.prop }) expect(dummy).toBe(0) obj.foo.prop++ expect(dummy).toBe(0) obj.foo = { prop: 1 } expect(dummy).toBe(1) }) // 设置 NaN 不会被多次触发跟踪回调 it('should not be trigger when the value and the old value both are NaN', () => { const obj = reactive({ foo: NaN }) const fnSpy = jest.fn(() => obj.foo) effect(fnSpy) obj.foo = NaN expect(fnSpy).toHaveBeenCalledTimes(1) }) })
effect 实现跟踪的原理简单分析
先回忆一下,上次关于 reactive 的分享中,在创建
proxy
后会对targetMap
来一个set
操作:以下内容来自: vue-next/packages/reactivity/src/reactive.ts
function createReactiveObject( target: unknown, toProxy: WeakMap<any, any>, toRaw: WeakMap<any, any>, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any> ) { // ... const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers observed = new Proxy(target, handlers) toProxy.set(target, observed) toRaw.set(observed, target) if (!targetMap.has(target)) { // 注意这里 set 了一个 map targetMap.set(target, new Map()) } return observed }
然后
handlers
中都有调用track
或trigger
。比如对象的
Get
和Set
:以下内容来自: vue-next/packages/reactivity/src/baseHandlers.ts
function createGetter(isReadonly: boolean) { return function get(target: object, key: string | symbol, receiver: object) { // ... track(target, OperationTypes.GET, key) // 这个 track 是引用的 effect.ts // ... } } function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { // ... // don't trigger if target is something up in the prototype chain of original if (target === toRaw(receiver)) { /* istanbul ignore else */ // 这里区分开发环境与生产环境,主要是了 debug 使用(vue-tools 留接口?) if (__DEV__) { const extraInfo = { oldValue, newValue: value } if (!hadKey) { trigger(target, OperationTypes.ADD, key, extraInfo) } else if (hasChanged(value, oldValue)) { trigger(target, OperationTypes.SET, key, extraInfo) } } else { if (!hadKey) { trigger(target, OperationTypes.ADD, key) } else if (hasChanged(value, oldValue)) { trigger(target, OperationTypes.SET, key) } } } return result }
effect 实现流程简要描述
- 创建一个 reactive 对象 =>
obj
- 调用
effect
,传递回调函数,回调中涉及到了obj
的 get 操作,比如obj.a
- effect 内部执行
createReactiveEffect
- createReactiveEffect 内部添加一些 effect 的属性,然后 return 一个函数,函数内部又 return 了一个 run 函数
- run 函数判断 effectStack 是否包含了当前的 effect,如果没有则添加,然后执行第二步传递的回调
- 到这里
createReactiveEffect
执行完毕了,effect 内部判断option.lazy
是否立即执行一次createReactiveEffect
返回的函数(后面就当执行了) - 回调函数被调用了,执行到
obj.a
的时候,就出发了 proxy 的 get 处理方法,会调用track
方法 - track 根据 effectStack 找到当前的 effect(这里实现的比较秒,后面介绍),然后去
targetMap
找 map,最后把当前的 effect 函数 赛进去 - 当
obj.a
发生变化后,开始执行trigger
- trigger 函数相当于 track 的反操作,取出来然后执行(当然,实际是要比 track 复杂的多的多)
源码层面的理解
以下按照上面的步骤依次展示,内容来自 vue-next/packages/reactivity/src/effect.ts
// 对外 API,effect 函数 export function effect<T = any>( // 回调 fn: () => T, // 配置项 options: ReactiveEffectOptions = EMPTY_OBJ ): ReactiveEffect<T> { // 这里判断传入的 fn 是否已经是一个 effect 了。如果是获取源回调函数, if (isEffect(fn)) { fn = fn.raw } // 创建 effect,传入 回调与配置项 const effect = createReactiveEffect(fn, options) // 判断是否开启了 lazy 模式 if (!options.lazy) { // 没有开启,立即执行 effect effect() } return effect } // createReactiveEffect 函数 function createReactiveEffect<T = any>( fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { // 这里 effect 被赋值了一个函数 const effect = function reactiveEffect(...args: unknown[]): unknown { // 执行 reactiveEffect 后会 执行 run ,同时传递 effect, fn, args return run(effect, fn, args) } as ReactiveEffect // 给 effect 设置自身属性 effect._isEffect = true effect.active = true effect.raw = fn // 这里会缓存 track 的列表,后续用于 stop 将自己删除掉 effect.deps = [] effect.options = options return effect } // run 函数 function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown { // 判断是否被 stop 了 // 被 stop 后,只有主动调用 runner(调用 effect 返回的函数)才会走到这里 if (!effect.active) { return fn(...args) } // 判断响应队列是包含了 当前的 effect if (!effectStack.includes(effect)) { // 不是太理解 为什么每次都要 cleanup? cleanup(effect) try { // 注意这里 push 了一次 effect effectStack.push(effect) return fn(...args) } finally { // 等 fn 执行完后,立刻取出 effect effectStack.pop() } } } // 假设 effect 已调用一次,那么这里已经触发了 get 代理事件,也就是执行了 track 函数 export function track(target: object, type: OperationTypes, key?: unknown) { // 判断 effectStack 队列是否有值 // shouldTrack 应该是开放模式下 vue-tools 可以暂停跟踪的功能 if (!shouldTrack || effectStack.length === 0) { return } // 注意这里 为什么是 effectStack[effectStack.length - 1] // 最后一个就是当前的 effect 吗? const effect = effectStack[effectStack.length - 1] if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } // 从 targetMap 中取值,用于添加涉及到的 effect!!! let depsMap = targetMap.get(target) if (depsMap === void 0) { targetMap.set(target, (depsMap = new Map())) } // targetMap 是 weekMap // depsMap 是 Map // dep 是 Set() // 这里有个 key,是 target 上面的属性,所以这里的 effect 存储也按对应的字段区分开了 let dep = depsMap.get(key!) if (dep === void 0) { depsMap.set(key!, (dep = new Set())) } // 将当前 effect 放入, 供后面 trigger 使用 if (!dep.has(effect)) { dep.add(effect) effect.deps.push(dep) if (__DEV__ && effect.options.onTrack) { effect.options.onTrack({ effect, target, type, key }) } } } // const effect = effectStack[effectStack.length - 1] 说明 // 因为上面 run 函数中 push 之后,立刻执行了 fn(),fn 中又触发了 代理钩子 // 代理钩子中又调用了 track // 因为 js 单线程的机制,effectStack.length - 1 永远会是 run 函数中 push 的那一个 // trigger 函数 export function trigger( target: object, type: OperationTypes, key?: unknown, extraInfo?: DebuggerEventExtraInfo ) { // 从 targetMap 中获取 depsMap const depsMap = targetMap.get(target) // 表示没有被 effect 调用过 if (depsMap === void 0) { // never been tracked return } // 涉及到的 effect 存放(比如有获取 obj.a 操作的 effect) const effects = new Set<ReactiveEffect>() // 计算属性。 const computedRunners = new Set<ReactiveEffect>() // 下面会根据 key 获取对应的 effect // clear 的时候 不区分字段了 all in(数组的时候才会有CLEAR) if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { // addRuners 只是取出 effect 放入到 effects 和 computedRunners,因为内部有 if 逻辑,所以抽出来一个函数 addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { // 只取出 key 对应的 effect 放入对应的集合 addRunners(effects, computedRunners, depsMap.get(key)) } // also run for iteration key on ADD | DELETE if (type === OperationTypes.ADD || type === OperationTypes.DELETE) { const iterationKey = Array.isArray(target) ? 'length' : ITERATE_KEY addRunners(effects, computedRunners, depsMap.get(iterationKey)) } } // 使用调度器 运行传入的 effect const run = (effect: ReactiveEffect) => { scheduleRun(effect, target, type, key, extraInfo) } // Important: computed effects must be run first so that computed getters // can be invalidated before any normal effects that depend on them are run. // 这里不太明白 为什么 computedRunners 要先运行 computedRunners.forEach(run) effects.forEach(run) } // 只是取出 effect 放入到 effects 和 computedRunners function addRunners( effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.options.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } } // 调度器 function scheduleRun( effect: ReactiveEffect, target: object, type: OperationTypes, key: unknown, extraInfo?: DebuggerEventExtraInfo ) { if (__DEV__ && effect.options.onTrigger) { const event: DebuggerEvent = { effect, target, key, type } effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event) } // 很简单,传递调度器了,调用调度器函数,否则 执行 effect if (effect.options.scheduler !== void 0) { effect.options.scheduler(effect) } else { effect() } }
- 创建一个 reactive 对象 =>