前言
前两篇《Vue 3 Pre-Alpha / vue-next 源码学习之 vue/reactivity/reactive.ts》和《Vue 3 Pre-Alpha / vue-next 源码学习之 vue/reactivity/ref.ts》中,我们学习了如何把目标数据或者目标对象封装成了响应式的或者对象,但除用于响应数据读写变化的方法(在 vue 3 中会把用户传入的响应方法/响应函数再次封装,然后成为本篇介绍的 effect
或者叫 ReactiveEffect
),我们还没有涉及,现在下面主要对结合一个简单的例子及 effect
的源码进行解读。
一个简单的例子
下面是自己动手,对一个简单的加法(plus
)函数通过 createEffect
方法创建出两个新的 effect
函数 plusEffect1
和 plusEffect2
,这两个函数跟原始的 plus
一样可以执行加法,但被附加上了一个自己的 options
属性
const run = (fn, args) => {
return fn(...args)
}
const plus = (a, b) => a + b
const createEffect = (fn, options) => {
function effect() {
return run(fn, [...arguments])
}
effect.options = options // 在 Vue 3 的 effect 中,通过 options 把 effectSymbol, active, raw, onTrack 等附加进来
return effect
}
const plusEffect1 = createEffect(plus, { name: 'plusEffect1' })
plusEffect1(1, 2) // => 3
plusEffect1.options.name // => plusEffect1
const plusEffect2 = createEffect(plus, { name: 'plusEffect1' })
plusEffect2(1, 2) // => 3
plusEffect2.options.name // => plusEffect2
源码分析
import { OperationTypes } from './operations' // set, add, delete, clear, get, has, iterate 操作类型 import { Dep, targetMap } from './reactive' // Dep 是 Set<ReactiveEffect>, KeyToDepMap 是 Map<string | symbol, Dep>, targetMap 是 WeakMap<any, KeyToDepMap> import { EMPTY_OBJ, extend } from '@vue/shared' // 工具函数 export const effectSymbol = Symbol(__DEV__ ? 'effect' : void 0) export interface ReactiveEffect<T = any> { // effect 的定义 (): T [effectSymbol]: true active: boolean raw: () => T deps: Array<Dep> computed?: boolean scheduler?: (run: Function) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void } export interface ReactiveEffectOptions { // effect options 的定义 lazy?: boolean computed?: boolean scheduler?: (run: Function) => void onTrack?: (event: DebuggerEvent) => void onTrigger?: (event: DebuggerEvent) => void onStop?: () => void } export interface DebuggerEvent { // 用于调试 effect: ReactiveEffect target: any type: OperationTypes key: string | symbol | undefined } export const effectStack: ReactiveEffect[] = [] // effect stack 临时数组,当 effect run 完之后就会被 pop 出来 export const ITERATE_KEY = Symbol('iterate') // 使用唯一标识,Symbol 可以创建私有变量作用 export function isEffect(fn: any): fn is ReactiveEffect { // 函数是否为 effect,给 effect 添加了一个私有标识,通过 Symbol 实现 return fn != null && fn[effectSymbol] === true } export function effect<T = any>( fn: () => T, options: ReactiveEffectOptions = EMPTY_OBJ // options 初始为空对象 ): ReactiveEffect<T> { if (isEffect(fn)) { // 如果函数已经被封装成 effect 就把 raw 函数取出来 fn = fn.raw } const effect = createReactiveEffect(fn, options) // 创建 efect if (!options.lazy) { // 如果不是异步执行,马上执行 effect() } return effect } export function stop(effect: ReactiveEffect) { // 停止事件的 effect,类似于 removeEventListener if (effect.active) { cleanup(effect) if (effect.onStop) { // 有 onStop 勾子,则执行 effect.onStop() } effect.active = false // 停止事件的 effect,其实就是把其 active 属性改成 false } } function createReactiveEffect<T = any>( // 创建一个 effect,其实 effect 就是变种的 fn,根据 options 给 fn 附加一系列的勾子函数及属性 fn: () => T, options: ReactiveEffectOptions ): ReactiveEffect<T> { const effect = function reactiveEffect(...args: any[]): any { return run(effect, fn, args) } as ReactiveEffect effect[effectSymbol] = true effect.active = true effect.raw = fn effect.scheduler = options.scheduler effect.onTrack = options.onTrack effect.onTrigger = options.onTrigger effect.onStop = options.onStop effect.computed = options.computed effect.deps = [] return effect } function run(effect: ReactiveEffect, fn: Function, args: any[]): any { // 执行 effect / fn 函数 if (!effect.active) { return fn(...args) } if (!effectStack.includes(effect)) { // 如果这个 effect 还在 effectStack 里面,未执行完,不需要任何操作 cleanup(effect) try { effectStack.push(effect) // 入栈 effectStack return fn(...args) } finally { effectStack.pop() // 执行完后,就要把 effect 由 effectStack 中 pop 出来 } } } function cleanup(effect: ReactiveEffect) { // 顾名思义,就是清空 effect.deps const { deps } = effect if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect) } deps.length = 0 } } let shouldTrack = true // 要不要 track,默认是要的 export function pauseTracking() { // 暂停 track shouldTrack = false } export function resumeTracking() { // 继续 track shouldTrack = true } export function track( // 跟踪 effect 是由什么操作触发 target: any, type: OperationTypes, key?: string | symbol ) { if (!shouldTrack || effectStack.length === 0) { return } const effect = effectStack[effectStack.length - 1] if (type === OperationTypes.ITERATE) { key = ITERATE_KEY } let depsMap = targetMap.get(target) // depsMap 由 targetMap(WeakMap)中取出 if (depsMap === void 0) { // 如果 targetMap 里面没有 target 这个 key, targetMap.set(target, (depsMap = new Map())) // 则追加 set 进去 } let dep = depsMap.get(key!) // depsMap 是 Map,可能通过 key 取到相应的 dep if (dep === void 0) { // 如果 dep 为空,则 set 一个 dep(类型是 Set) depsMap.set(key!, (dep = new Set())) // key! 是表示强制解析,表示 key 一定有值 } if (!dep.has(effect)) { dep.add(effect) // effect add 到 dep(Set)里面 effect.deps.push(dep) // 入 deps 栈 if (__DEV__ && effect.onTrack) { // dev 环境需要触发 onTrack effect.onTrack({ effect, target, type, key }) } } } export function trigger( // 按操作类型,触发执行 effect target: any, type: OperationTypes, key?: string | symbol, extraInfo?: any ) { const depsMap = targetMap.get(target) if (depsMap === void 0) { // 没被 tracked 过,直接返回,ref 创建 Ref 时,get 操作需要 track,set 操作需要 trigger // never been tracked return } const effects = new Set<ReactiveEffect>() const computedRunners = new Set<ReactiveEffect>() if (type === OperationTypes.CLEAR) { // collection being cleared, trigger all effects for target depsMap.forEach(dep => { addRunners(effects, computedRunners, dep) }) } else { // schedule runs for SET | ADD | DELETE if (key !== void 0) { 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)) } } const run = (effect: ReactiveEffect) => { // 注意:此 run 运行完后,是不需要入栈 effectStack,会临时入栈 computedRunners,跟外面那 run 函数不一样 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.forEach(run) // 运行所有 computed 的effect, effects.forEach(run) // 运行所有的 effect } function addRunners( // runners 入栈 effects: Set<ReactiveEffect>, computedRunners: Set<ReactiveEffect>, effectsToAdd: Set<ReactiveEffect> | undefined ) { if (effectsToAdd !== void 0) { effectsToAdd.forEach(effect => { if (effect.computed) { computedRunners.add(effect) } else { effects.add(effect) } }) } } function scheduleRun( // 计划执行 effect effect: ReactiveEffect, target: any, type: OperationTypes, key: string | symbol | undefined, extraInfo: any ) { if (__DEV__ && effect.onTrigger) { effect.onTrigger( extend( { effect, target, key, type }, extraInfo ) ) } if (effect.scheduler !== void 0) { effect.scheduler(effect) } else { effect() } }
总结
通过对 effect
源码的分析,结合之前对 ref
及 reactive
的理解,可以想象得到作者在规划新版的 Vue 3 的 reactivity
一些思路及方向:先对用户传入的状态数据或者状态变量进行二次封装,目标是达到数据的读写都是可被监听的;然后对用户传入的针对数据读写的响应方法进行二次封装,让响应方法的执行也是可被监听的。总之,目标是对数据的读写变化及响应数据的变化而执行相应的事件及方法,整个流程下都是、可控的、可调试、可追踪、可额外加入其他的 Vue 需要的属性及勾子函数等等。
问题思考:couter1
的值发生变化,没有触发第二个 effect
执行,没有改变了 dummy2
的值,这是为什么呢?
在 \packages\reactivity\__tests__\effect.spec.ts
文件添加一个测试用例
it('should observe 2 different basic reactive effects', () => {
let dummy1
const counter1 = reactive({ num: 0 })
effect(() => (dummy1 = counter1.num)) // 第一个effect,里面包含 counter1 的读操作
let dummy2
const counter2 = reactive({ num: 0 })
const fnSpy = jest.fn(() => (dummy2 = ++counter2.num))
effect(fnSpy) // 第二个effect,里面包含 counter2 的读操作
expect(fnSpy).toHaveBeenCalledTimes(1)
expect(dummy1).toBe(0)
expect(dummy2).toBe(1)
counter1.num = 7
expect(dummy1).toBe(7) // couter1 的值发生变化,触发第一个 effect 执行,改变了 dummy1 的值
expect(dummy2).toBe(1) // couter1 的值发生变化,没有触发第二个 effect 执行,没有改变了 dummy2 的值,这是为什么呢?
expect(fnSpy).toHaveBeenCalledTimes(1) // couter1 的值发生变化,没有触发第二个 effect 执行
})
实现原理
reactive
对象与 effect
是通过 targetMap
来建立绑定关系的