Vue3源码解读effectScope API及实现原理
作者:PHM 时间:2023-12-11 19:28:49
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()
...
}
来源:https://juejin.cn/post/7215484775640088613