Vue3讀原始碼系列(七):effectScope API實現原理
theme: channing-cyan
vue3新增了effectScope相關的API,其官方的描述是建立一個 effect 作用域,可以捕獲其中所建立的響應式副作用 (即計算屬性和偵聽器),這樣捕獲到的副作用可以一起處理。並給出了示例: ```ts 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
ts
// packages/reactivity/src/effectScope.ts
export function effectScope(detached?: boolean) {
// 返回EffectScope例項
return new EffectScope(detached)
}
EffectScope
```ts 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 }
runcannot 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.cleanupsi
}
// 遞迴停止所有的子作用域
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的建構函式:
ts
// 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物件了:
ts
const instance: ComponentInternalInstance = {
...
// 初始化scope
scope: new EffectScope(true /* detached */),
...
}
在我們執行setup之前,會呼叫setCurrentInstance,他會呼叫instance.scope.on,那麼就會將activeEffectScope賦值為instance.scope,那麼在setup中註冊的computed、watch等就都會被收集到instance.scope.effects
ts
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的收集是同樣的操作:
ts
// support for 2.x options
if (__FEATURE_OPTIONS_API__ && !(__COMPAT__ && skipOptions)) {
setCurrentInstance(instance)
pauseTracking()
// 處理options API
applyOptions(instance)
resetTracking()
unsetCurrentInstance()
}
完成了收集那麼對於清理就只需要在元件解除安裝的時候執行stop方法即可:
```ts
// 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() ... } ```