副作用函數是指會產生副作用的函數,如下面的程式碼所示:
function effect(){ document.body.innerText = 'hello vue3' }
當effect 函數執行時,它會設定body 的文字內容,但除了effect 函數之外的任何函數都可以讀取或設定body 的文字內容。也就是說,effect 函數的執行會直接或間接影響其他函數的執行,這時我們說 effect 函數產生了副作用。副作用很容易產生,例如一個函數修改了全域變量,這其實也是一個副作用。
// 全局变量 let val = 1 function effect() { val = 2 // 修改全局变量,产生副作用 }
在副作用模組中,定義了幾個全域的變量,提前認識這些變數有助於與我們了解副作用函數的產生以及呼叫的過程。
// packages/reactivity/src/effect.ts export type Dep = Set<ReactiveEffect> & TrackedMarkers type KeyToDepMap = Map<any, Dep> // WeakMap 集合存储副作用函数 const targetMap = new WeakMap<any, KeyToDepMap>() // 用一个全局变量存储当前激活的 effect 函数 export let activeEffect: ReactiveEffect | undefined // 标识是否开启了依赖收集 export let shouldTrack = true const trackStack: boolean[] = []
targetMap 是一個WeakMap 類型的集合,用來儲存副作用函數,從型別定義可以看出targetMap的資料結構方式:
#WeakMap 由target --> Map
構成
#Map 由key --> Set
構成
#其中WeakMap 的鍵是原始物件target,WeakMap 的值是一個Map 實例,Map 的鍵是原始物件target 的key,Map 的值是一個由副作用函數組成的Set。它們的關係如下:
targetMap 為什麼要用WeakMap我們來看下面的程式碼:const map = new Map(); const weakMap = new WeakMap(); (function() { const foo = {foo: 1}; const bar = {bar: 2}; map.set(foo, 1); // foo 对象是 map 的key weakMap.set(bar, 2); // bar 对象是 weakMap 的 key })
垃圾回收器
不會把它從記憶體中移除,我們仍然可以透過map.keys 列印出物件foo 。 對於物件bar 來說,由於WeakMap 的key 是弱引用,它不影響垃圾收集器的工作,所以一旦表達式執行完畢,垃圾回收器就會把物件bar 從記憶體中移除,而我們無法取得weakMap 的key 值,也就無法透過weakMap 取得物件bar 。 簡單地說,WeakMap 對 key 是弱引用,不影響垃圾回收器的工作**。根據這個特性可知,一旦 key 被垃圾回收器回收,那麼對應的鍵和值就無法存取了。所以 WeakMap 常用來儲存那些只有當 key 所引用的物件存在時 (沒有被回收) 才有價值的資訊**。
例如在上面的場景中,如果 target 物件沒有任何引用了,表示使用者側不再需要它了,這時垃圾回收器會完成回收任務。但如果使用 Map 來代替 WeakMap,那麼即使用戶側的程式碼對 target 沒有任何引用,這個 target 也不會被回收,最終可能導致記憶體溢出。 activeEffectactiveEffect 變數用來維護目前正在執行的副作用shouldTrack######shouldTrack 變數用來識別是否開啟依賴蒐集,只有shouldTrack 的值為true 時,才進行依賴收集,即將副作用函數加入到依賴集合中。 ######副作用的實作######effect 函數######effect API 用來建立副作用函數,接受兩個參數,分別是使用者自訂的fn函數和options 選項。原始碼如下所示:###// packages/reactivity/src/effect.ts export function effect<T = any>( fn: () => T, options?: ReactiveEffectOptions ): ReactiveEffectRunner { // 当传入的 fn 中存在 effect 副作用时,将这个副作用的原始函数赋值给 fn if ((fn as ReactiveEffectRunner).effect) { fn = (fn as ReactiveEffectRunner).effect.fn } // 创建一个副作用 const _effect = new ReactiveEffect(fn) if (options) { extend(_effect, options) if (options.scope) recordEffectScope(_effect, options.scope) } // 如果不是延迟执行的,则立即执行一次副作用函数 if (!options || !options.lazy) { _effect.run() } // 通过 bind 函数返回一个新的副作用函数 const runner = _effect.run.bind(_effect) as ReactiveEffectRunner // 将副作用添加到新的副作用函数上 runner.effect = _effect // 返回这个新的副作用函数 return runner }
// packages/reactivity/src/effect.ts export class ReactiveEffect<T = any> { active = true deps: Dep[] = [] parent: ReactiveEffect | undefined = undefined /** * Can be attached after creation * @internal */ computed?: ComputedRefImpl<T> /** * @internal */ allowRecurse?: boolean onStop?: () => void // dev only onTrack?: (event: DebuggerEvent) => void // dev only onTrigger?: (event: DebuggerEvent) => void constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope ) { recordEffectScope(this, scope) } run() { // 如果 effect 已停用,返回原始副作用函数执行后的结果 if (!this.active) { return this.fn() } let parent: ReactiveEffect | undefined = activeEffect let lastShouldTrack = shouldTrack while (parent) { if (parent === this) { return } parent = parent.parent } try { // 创建一个新的副作用前将当前正在执行的副作用存储到新建的副作用的 parent 属性上,解决嵌套effect 的情况 this.parent = activeEffect // 将创建的副作用设置为当前正则正在执行的副作用 activeEffect = this // 将 shouldTrack 设置为 true,表示开启依赖收集 shouldTrack = true trackOpBit = 1 << ++effectTrackDepth if (effectTrackDepth <= maxMarkerBits) { // 初始化依赖 initDepMarkers(this) } else { // 清除依赖 cleanupEffect(this) } // 返回原始副作用函数执行后的结果 return this.fn() } finally { if (effectTrackDepth <= maxMarkerBits) { finalizeDepMarkers(this) } trackOpBit = 1 << --effectTrackDepth // 重置当前正在执行的副作用 activeEffect = this.parent shouldTrack = lastShouldTrack this.parent = undefined } } // 停止(清除) effect stop() { if (this.active) { cleanupEffect(this) if (this.onStop) { this.onStop() } this.active = false } } }
全局变量 activeEffect 用来维护当前正在执行的副作用,当存在嵌套渲染组件的时候,依赖收集后,副作用函数会被覆盖,即 activeEffect 存储的副作用函数在嵌套 effect 的时候会被内层的副作用函数覆盖。为了解决这个问题,在 run 方法中,将当前正在执行的副作用activeEffect保存到新建的副作用的 parent 属性上,然后再将新建的副作用设置为当前正在执行的副作用。在新建的副作用执行完毕后,再将存储到 parent 属性的副作用重新设置为当前正在执行的副作用。
在 ReactiveEffect 类中,还定义了一个 stop 方法,该方法用来停止并清除当前正在执行的副作用。
当使用代理对象访问对象的属性时,就会触发代理对象的 get 拦截函数执行,如下面的代码所示:
const obj = { foo: 1 } const p = new Proxy(obj, { get(target, key, receiver) { track(target, key) return Reflect.get(target, key, receiver) } }) p.foo
在上面的代码中,通过代理对象p 访问 foo 属性,便会触发 get 拦截函数的执行,此时就在 get 拦截函数中调用 track 函数进行依赖收集。源码中 get 拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理》一文中的「访问属性的拦截」小节。
下面,我们来看看 track 函数的实现。
// packages/reactivity/src/effect.ts // 收集依赖 export function track(target: object, type: TrackOpTypes, key: unknown) { // 如果开启了依赖收集并且有正在执行的副作用,则收集依赖 if (shouldTrack && activeEffect) { // 在 targetMap 中获取对应的 target 的依赖集合 let depsMap = targetMap.get(target) if (!depsMap) { // 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map() targetMap.set(target, (depsMap = new Map())) } // 从依赖集合中获取对应的 key 的依赖 let dep = depsMap.get(key) if (!dep) { // 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set() depsMap.set(key, (dep = createDep())) } const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo) } }
在 track 函数中,通过一个 if 语句判断是否进行依赖收集,只有当 shouldTrack 为 true 并且存在 activeEffect,即开启了依赖收集并且存在正在执行的副作用时,才进行依赖收集。
然后通过 target 对象从 targetMap 中尝试获取对应 target 的依赖集合depsMap,如果 targetMap 中不存在当前target的依赖集合,则将当前 target 添加进 targetMap 中,并将 targetMap 的 value 初始化为 new Map()。
// 在 targetMap 中获取对应的 target 的依赖集合 let depsMap = targetMap.get(target) if (!depsMap) { // 如果 target 不在 targetMap 中,则加入,并初始化 value 为 new Map() targetMap.set(target, (depsMap = new Map())) }
接着根据target中被读取的 key,从依赖集合depsMap中获取对应 key 的依赖,如果依赖不存在,则将这个 key 的依赖收集到依赖集合depsMap中,并将依赖初始化为 new Set()。
// 从依赖集合中获取对应的 key 的依赖 let dep = depsMap.get(key) if (!dep) { // 如果 key 不存在,将这个 key 作为依赖收集起来,并初始化 value 为 new Set() depsMap.set(key, (dep = createDep())) }
最后调用 trackEffects 函数,将副作用函数收集到依赖集合depsMap中。
const eventInfo = __DEV__ ? { effect: activeEffect, target, type, key } : undefined trackEffects(dep, eventInfo)
// 收集副作用函数 export function trackEffects( dep: Dep, debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { let shouldTrack = false if (effectTrackDepth <= maxMarkerBits) { if (!newTracked(dep)) { dep.n |= trackOpBit // set newly tracked shouldTrack = !wasTracked(dep) } } else { // Full cleanup mode. // 如果依赖中并不存当前的 effect 副作用函数 shouldTrack = !dep.has(activeEffect!) } if (shouldTrack) { // 将当前的副作用函数收集进依赖中 dep.add(activeEffect!) // 并在当前副作用函数的 deps 属性中记录该依赖 activeEffect!.deps.push(dep) if (__DEV__ && activeEffect!.onTrack) { activeEffect!.onTrack( Object.assign( { effect: activeEffect! }, debuggerEventExtraInfo ) ) } } }
在 trackEffects 函数中,检查当前正在执行的副作用函数 activeEffect 是否已经被收集到依赖集合中,如果没有,就将当前的副作用函数收集到依赖集合中。同时在当前副作用函数的 deps 属性中记录该依赖。
当对属性进行赋值时,会触发代理对象的 set 拦截函数执行,如下面的代码所示:
const obj = { foo: 1 } const p = new Proxy(obj, { // 拦截设置操作 set(target, key, newVal, receiver){ // 如果属性不存在,则说明是在添加新属性,否则设置已有属性 const type = Object.prototype.hasOwnProperty.call(target,key) ? 'SET' : 'ADD' // 设置属性值 const res = Reflect.set(target, key, newVal, receiver) // 把副作用函数从桶里取出并执行,将 type 作为第三个参数传递给 trigger 函数 trigger(target,key,type) return res } // 省略其他拦截函数 }) p.foo = 2
在上面的代码中,通过代理对象p 访问 foo 属性,便会触发 set 拦截函数的执行,此时就在 set 拦截函数中调用 trigger 函数中派发更新。源码中 set 拦截函数的解析可阅读《Vue3 源码解读之非原始值的响应式原理》一文中的「设置属性操作的拦截」小节。
下面,我们来看看 track 函数的实现。
trigger 函数的源码如下:
export function trigger( target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown> ) { const depsMap = targetMap.get(target) // 该 target 从未被追踪,则不继续执行 if (!depsMap) { // never been tracked return } // 存放所有需要派发更新的副作用函数 let deps: (Dep | undefined)[] = [] if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target // 当需要清除依赖时,将当前 target 的依赖全部传入 deps = [...depsMap.values()] } else if (key === 'length' && isArray(target)) { // 处理数组的特殊情况 depsMap.forEach((dep, key) => { // 如果对应的长度, 有依赖收集需要更新 if (key === 'length' || key >= (newValue as number)) { deps.push(dep) } }) } else { // schedule runs for SET | ADD | DELETE // 在 SET | ADD | DELETE 的情况,添加当前 key 的依赖 if (key !== void 0) { deps.push(depsMap.get(key)) } // also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { // 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行 deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes deps.push(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { // 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行 deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)) } break } } const eventInfo = __DEV__ ? { target, type, key, newValue, oldValue, oldTarget } : undefined if (deps.length === 1) { if (deps[0]) { if (__DEV__) { triggerEffects(deps[0], eventInfo) } else { triggerEffects(deps[0]) } } } else { const effects: ReactiveEffect[] = [] // 将需要执行的副作用函数收集到 effects 数组中 for (const dep of deps) { if (dep) { effects.push(...dep) } } if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } } }
在 trigger 函数中,首先检查当前 target 是否有被追踪,如果从未被追踪过,即target的依赖未被收集,则不需要执行派发更新,直接返回即可。
const depsMap = targetMap.get(target) // 该 target 从未被追踪,则不继续执行 if (!depsMap) { // never been tracked return }
接着创建一个 Set 类型的 deps 集合,用来存储当前target的这个 key 所有需要执行派发更新的副作用函数。
// 存放所有需要派发更新的副作用函数 let deps: (Dep | undefined)[] = []
接下来就根据操作类型type 和 key 来收集需要执行派发更新的副作用函数。
如果操作类型是 TriggerOpTypes.CLEAR ,那么表示需要清除所有依赖,将当前target的所有副作用函数添加到 deps 集合中。
if (type === TriggerOpTypes.CLEAR) { // collection being cleared // trigger all effects for target // 当需要清除依赖时,将当前 target 的依赖全部传入 deps = [...depsMap.values()] }
如果操作目标是数组,并且修改了数组的 length 属性,需要把与 length 属性相关联的副作用函数以及索引值大于或等于新的 length 值元素的相关联的副作用函数从 depsMap 中取出并添加到 deps 集合中。
else if (key === 'length' && isArray(target)) { // 如果操作目标是数组,并且修改了数组的 length 属性 depsMap.forEach((dep, key) => { // 对于索引大于或等于新的 length 值的元素, // 需要把所有相关联的副作用函数取出并添加到 deps 中执行 if (key === 'length' || key >= (newValue as number)) { deps.push(dep) } }) }
如果当前的 key 不为 undefined,则将与当前key相关联的副作用函数添加到 deps 集合中。注意这里的判断条件 void 0,是通过 void 运算符的形式表示 undefined 。
if (key !== void 0) { deps.push(depsMap.get(key)) }
接下来通过 Switch 语句来收集操作类型为 ADD、DELETE、SET 时与 ITERATE_KEY 和 MAP_KEY_ITERATE_KEY 相关联的副作用函数。
// also run for iteration key on ADD | DELETE | Map.SET switch (type) { case TriggerOpTypes.ADD: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { // 操作类型为 ADD 时触发Map 数据结构的 keys 方法的副作用函数重新执行 deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } else if (isIntegerKey(key)) { // new index added to array -> length changes deps.push(depsMap.get('length')) } break case TriggerOpTypes.DELETE: if (!isArray(target)) { deps.push(depsMap.get(ITERATE_KEY)) if (isMap(target)) { // 操作类型为 DELETE 时触发Map 数据结构的 keys 方法的副作用函数重新执行 deps.push(depsMap.get(MAP_KEY_ITERATE_KEY)) } } break case TriggerOpTypes.SET: if (isMap(target)) { deps.push(depsMap.get(ITERATE_KEY)) } break }
最后调用 triggerEffects 函数,传入收集的副作用函数,执行派发更新。
const eventInfo = __DEV__ ? { target, type, key, newValue, oldValue, oldTarget } : undefined if (deps.length === 1) { if (deps[0]) { if (__DEV__) { triggerEffects(deps[0], eventInfo) } else { triggerEffects(deps[0]) } } } else { const effects: ReactiveEffect[] = [] // 将需要执行的副作用函数收集到 effects 数组中 for (const dep of deps) { if (dep) { effects.push(...dep) } } if (__DEV__) { triggerEffects(createDep(effects), eventInfo) } else { triggerEffects(createDep(effects)) } }
export function triggerEffects( dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo ) { // spread into array for stabilization // 遍历需要执行的副作用函数集合 for (const effect of isArray(dep) ? dep : [...dep]) { // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行 if (effect !== activeEffect || effect.allowRecurse) { if (__DEV__ && effect.onTrigger) { effect.onTrigger(extend({ effect }, debuggerEventExtraInfo)) } if (effect.scheduler) { // 如果一个副作用函数存在调度器,则调用该调度器 effect.scheduler() } else { // 否则直接执行副作用函数 effect.run() } } } }
在 triggerEffects 函数中,遍历需要执行的副作用函数集合,如果当前副作用函数存在调度器,则执行该调度器,否则直接执行该副作用函数的 run 方法,执行更新。
以上是Vue3之副作用函數與依賴收集實例分析的詳細內容。更多資訊請關注PHP中文網其他相關文章!