effectScope 是 Vue 3.2.0 引入的新 API,属于响应式系统的高阶内容。从字面上理解,它就是 effect 作用域,用于收集在其中所创建的副作用,并能对其进行统一的处理。
除非是开发独立的库,我们几乎不会用到 effectScope。尽管如此,了解 effectScope 对于我们理解 Vue 3 源码或是其它开源库(比如 VueUse)还是很有必要的。
// 在 scope 中创建的 effect computed watch watchEffect 都会被收集起来
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理 scope 中所有的副作用
scope.stop()
首先通过 effectScope 创建 scope 的本质其实是下面这样。
export function effectScope(detached?: boolean) {
return new EffectScope(detached)
}
对 new 操作进行了一次包装。注意这里有一个可选的 detached 参数,表示该作用域是否是独立的(是否存在父级作用域)。
然后通过 run 方法将入参函数中的副作用限定在当前作用域内,最后通过 stop 方法清除当前作用域内的所有副作用。
上面这个过程在组件 setup 内同样存在。组件实例被创建的同时会创建一个独立的 scope。
export function createComponentInstance(...args) {
// ...
const instance = {
// ...
vnode,
type,
scope: new EffectScope(true /* detached */),
// ...
}
return instance
}
当前实例中的副作用都会被收集到 scope 的 effects 中。在组件卸载时这些副作用会自动清除。
const unmountComponent = (...args) => {
// ...
scope.stop()
// ...
}
整个过程都是 Vue 内部处理的,我们不需要关心副作用的收集和清除。
除了前面提到的 effectScope(),Vue 还为我们提供了 getCurrentScope() 用于获取当前的作用域以及 onScopeDispose() 来注册副作用清除的回调函数,这些 API 的基本使用可参考官方文档。
下面会对 API 使用上的细节做一些说明。
scope.stop() 的执行会清除当前作用域及其子作用域(递归地)的全部副作用。
function stop() {
// 清除当前作用域的副作用
for (i = 0, l = this.effects.length; i < l; i++) {
this.effects[i].stop()
}
// 清除子作用域的副作用
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i++) {
this.scopes[i].stop(true)
}
}
}
这里要注意,如果子作用域是独立的(detached = true),它是不会被父作用域收集的,自然地,在父作用域清除副作用时是不会清除该独立子作用域中的副作用的。
scope.run() 可以重复执行,对副作用的收集和清除会进行合并。
let dummy, doubled
let dummy1 = 0
const counter = reactive({ num: 0 })
const scope = effectScope()
scope.run(() => {
effect(() => (dummy = counter.num))
onScopeDispose(() => (dummy += 1))
onScopeDispose(() => (dummy += 2))
})
// scope.effects.length = 1
scope.run(() => {
effect(() => (doubled = counter.num * 2))
onScopeDispose(() => (dummy += 4))
})
// scope.effects.length = 2
counter.num = 7
// dummy = 7
// dummy1 = 0
// doubled = 14
scope.stop()
// dummy1 = 7