副作用関数とは、次のコードに示すように、副作用を生成する関数を指します。
function effect(){ document.body.innerText = 'hello vue3' }
効果関数が実行されると、ボディのテキストコンテンツですが、エフェクト関数を除くすべての関数がボディのテキストコンテンツを読み取りまたは設定できます。つまり、エフェクト関数の実行は他の関数の実行に直接的または間接的に影響を及ぼしますが、このときエフェクト関数には副作用があるといいます。副作用は発生しやすく、たとえば、関数がグローバル変数を変更する場合、これは実際には副作用です。
// 全局变量 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
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 を出力します。
オブジェクト バーの場合、WeakMap のキーは弱参照であるため、ガベージ コレクターの動作には影響しません。そのため、式が実行されると、ガベージ コレクターはオブジェクト バーをメモリから削除します。また、weakMap のキー値を取得できないため、weakMap を介してオブジェクト バーを取得することはできません。 簡単に言えば、WeakMap はキーへの弱い参照であり、ガベージ コレクターの動作には影響しません。**。この機能によれば、キーがガベージ コレクターによってリサイクルされると、対応するキーと値にはアクセスできなくなります。したがって、WeakMap は、キーによって参照されるオブジェクトが存在する (リサイクルされていない) 場合にのみ価値のある情報を格納するためによく使用されます**。
たとえば、上記のシナリオでは、ターゲット オブジェクトに参照がない場合、ユーザー側でそのオブジェクトが不要になったことを意味し、ガベージ コレクターはリサイクル タスクを完了します。ただし、WeakMap の代わりに Map を使用すると、ユーザー側のコードにターゲットへの参照がない場合でも、ターゲットはリサイクルされず、最終的にメモリ オーバーフローが発生する可能性があります。 activeEffectactiveEffect 変数は、現在実行されている副作用を維持するために使用されます。 shouldTrack shouldTrack 変数は、依存関係の収集が有効かどうかを識別するために使用されます。 shouldTrack の値のみ true の場合、依存関係の収集が実行されます。つまり、副作用関数が依存関係の収集に追加されます。 副作用の実装効果関数効果 API は、ユーザー定義の fn 関数とオプションのオプションである 2 つのパラメーターを受け入れる副作用関数を作成するために使用されます。ソースコードは以下のとおりです。// 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 }
fn.effect.fn を渡す必要があります。
ReactiveEffect クラスは、副作用を作成するためにエフェクト関数内で呼び出されます。次に、ReactiveEffect クラスの実装を見てみましょう。 ReactiveEffect クラス// 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 中国語 Web サイトの他の関連記事を参照してください。