Vue3源码解读effectScope API及实现原理
作者:PHM
vue3新增effectScope相关的API
其官方的描述是创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。并给出了示例:
const scope = effectScope() scope.run(() => { const doubled = computed(() => counter.value * 2) watch(doubled, () => console.log(doubled.value)) watchEffect(() => console.log('Count: ', doubled.value)) }) // 处理掉当前作用域内的所有 effect scope.stop()
我们就从这个示例入手看看具体的源码实现:
effectScope
// packages/reactivity/src/effectScope.ts export function effectScope(detached?: boolean) { // 返回EffectScope实例 return new EffectScope(detached) }
EffectScope
export class EffectScope { /** * @internal */ private _active = true /** * @internal */ effects: ReactiveEffect[] = [] /** * @internal */ cleanups: (() => void)[] = [] /** * only assigned by undetached scope * @internal */ parent: EffectScope | undefined /** * record undetached scopes * @internal */ scopes: EffectScope[] | undefined /** * track a child scope's index in its parent's scopes array for optimized * // index作用:在父作用域数组中跟踪子作用域范围索引以进行优化。 * removal * @internal */ private index: number | undefined constructor(public detached = false) { // 记录当前scope为parent scope this.parent = activeEffectScope if (!detached && activeEffectScope) { this.index = (activeEffectScope.scopes || (activeEffectScope.scopes = [])).push( this ) - 1 } } get active() { return this._active } run<T>(fn: () => T): T | undefined { if (this._active) { const currentEffectScope = activeEffectScope try { activeEffectScope = this return fn() } finally { activeEffectScope = currentEffectScope } } else if (__DEV__) { warn(`cannot run an inactive effect scope.`) } } /** * This should only be called on non-detached scopes * 必须在非分离的作用域上调用 * @internal */ on() { activeEffectScope = this } /** * This should only be called on non-detached scopes * @internal */ off() { activeEffectScope = this.parent } // stop方法 stop(fromParent?: boolean) { if (this._active) { let i, l // stop effects for (i = 0, l = this.effects.length; i < l; i++) { this.effects[i].stop() } // 执行所有的cleanups for (i = 0, l = this.cleanups.length; i < l; i++) { this.cleanups[i]() } // 递归停止所有的子作用域 if (this.scopes) { for (i = 0, l = this.scopes.length; i < l; i++) { this.scopes[i].stop(true) } } // nested scope, dereference from parent to avoid memory leaks if (!this.detached && this.parent && !fromParent) { // optimized O(1) removal const last = this.parent.scopes!.pop() if (last && last !== this) { this.parent.scopes![this.index!] = last last.index = this.index! } } this.parent = undefined this._active = false } } }
在执行scope.run的时候会将this赋值到全局的activeEffectScope变量,然后执行传入函数。对于computed、watch、watchEffect(watchEffect是调用doWatch实现的,与watch实现响应式绑定的方式相同)这些API都会创建ReactiveEffect实例来建立响应式关系,而收集对应的响应式副作用就发生在ReactiveEffect创建的时候,我们来看一下ReactiveEffect的构造函数:
// ReactiveEffect的构造函数 constructor( public fn: () => T, public scheduler: EffectScheduler | null = null, scope?: EffectScope ) { // effect实例默认会被记录到指定scope中 // 如果没有指定scope则会记录到全局activeEffectScope中 recordEffectScope(this, scope) } // recordEffectScope实现 export function recordEffectScope( effect: ReactiveEffect, // scope默认值为activeEffectScope scope: EffectScope | undefined = activeEffectScope ) { if (scope && scope.active) { scope.effects.push(effect) } }
可以看到如果我们没有传入scope参数,那么在执行recordEffectScope时就会有一个默认的参数为activeEffectScope,这个值不正是我们scope.run的时候赋值的吗!所以新创建的effect会被放到activeEffectScope.effects中,这就是响应式副作用的收集过程。
那么对于一起处理就比较简单了,只需要处理scope.effects即可
组件的scope
日常开发中其实并不需要我们关心组件副作用的收集和清除,因为这些操作是已经内置好的,我们来看一下源码中是怎么做的
组件实例中的scope
在组件实例创建的时候就已经new了一个属于自已的scope对象了:
const instance: ComponentInternalInstance = { ... // 初始化scope scope: new EffectScope(true /* detached */), ... }
在我们执行setup之前,会调用setCurrentInstance,他会调用instance.scope.on,那么就会将activeEffectScope赋值为instance.scope,那么在setup中注册的computed、watch等就都会被收集到instance.scope.effects
function setupStatefulComponent( instance: ComponentInternalInstance, isSSR: boolean ) { // 组件对象 const Component = instance.type as ComponentOptions ... // 2. call setup() const { setup } = Component if (setup) { // 创建setupContext const setupContext = (instance.setupContext = // setup参数个数判断 大于一个参数创建setupContext setup.length > 1 ? createSetupContext(instance) : null) // instance赋值给currentInstance // 设置当前实例为instance 为了在setup中可以通过getCurrentInstance获取到当前实例 // 同时开启instance.scope.on() setCurrentInstance(instance) // 暂停tracking 暂停收集副作用函数 pauseTracking() // 执行setup const setupResult = callWithErrorHandling( setup, instance, ErrorCodes.SETUP_FUNCTION, // setup参数 [__DEV__ ? shallowReadonly(instance.props) : instance.props, setupContext] ) // 重新开启副作用收集 resetTracking() // currentInstance置为空 // activeEffectScope赋值为instance.scope.parent // 同时instance.scope.off() unsetCurrentInstance() ... } else { finishComponentSetup(instance, isSSR) } }
对于选项式API的收集是同样的操作:
// support for 2.x options if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) { setCurrentInstance(instance) pauseTracking() // 处理options API applyOptions(instance) resetTracking() unsetCurrentInstance() }
完成了收集那么对于清理就只需要在组件卸载的时候执行stop方法即可:
// packages/runtime-core/src/renderer.ts const unmountComponent = ( instance: ComponentInternalInstance, parentSuspense: SuspenseBoundary | null, doRemove?: boolean ) => { if (__DEV__ && instance.type.__hmrId) { unregisterHMR(instance) } const { bum, scope, update, subTree, um } = instance ... // stop effects in component scope // 副作用清除 scope.stop() ... }
以上就是Vue3源码解读effectScope API及实现原理的详细内容,更多关于Vue3 effectScope API的资料请关注脚本之家其它相关文章!