关于 Vue3 中的 EffectScope
Mon Aug 08 2022 Posted 2 years ago
时隔三个月后的第一篇博客,希望能终止我的颓势,让我能再向上爬一些。
最近在写公司业务的时候使用了 vueuse
中的 createSharedComposable
这个方法,用起来十分的顺手。于是乎找到了这个方法源码来读一读,然后发现读不懂...整个方法的实现不过20行代码,看起来十分简洁。其中最核心的部分就是引用的 Vue3.2 的新特性:EffectScope
。去翻了翻 Vue 文档,发现这个特性居然 antfu 大佬亲自提出来的,属于是自产自销了。
简单读了一下 EffectScope
的 RFC(主要是想深入读也没那个能力XD),终于是对这个方法有了一点头绪。
首先看一下 createSharedComposable
方法的实现:
import type { EffectScope } from 'vue-demi'
import { effectScope } from 'vue-demi'
import { tryOnScopeDispose } from '../tryOnScopeDispose'
/**
* Make a composable function usable with multiple Vue instances.
*
* @see https://vueuse.org/createSharedComposable
*/
export function createSharedComposable<Fn extends((...args: any[]) => any)>(composable: Fn): Fn {
let subscribers = 0
let state: ReturnType<Fn> | undefined
let scope: EffectScope | undefined
const dispose = () => {
subscribers -= 1
if (scope && subscribers <= 0) {
scope.stop()
state = undefined
scope = undefined
}
}
return <Fn>((...args) => {
subscribers += 1
if (!state) {
scope = effectScope(true)
state = scope.run(() => composable(...args))
}
tryOnScopeDispose(dispose)
return state
})
}
方法中定义了三个变量:subscribers
代表使用这个 composable 的组件数量,state
是被传入的方法的返回值, scope
则是上面提到的 EffectScope
该方法接收一个函数作为参数,返回值也是一个函数。首先将 subscribers ++
,然后去判断当前是否已经存在了 EffectScope
, 如果存在的话那就只绑定一个解绑事件并返回 state
,如果不存在的话,则创建一个 EffectScope
,并执行 scope.run
来获取传入的函数的返回值。
然后就是对 EffectScope
的相关概念讲解,此部分来自官方 RFC
出现的原因 #
在 Vue 的 setup 中,响应会在开始初始化的时候被收集,在实例被卸载的时候,响应就会自动的被取消追踪,这是一个很方便的特性。但是,当我们在组件外使用或者编写一个独立的包时,这会变得非常麻烦。当在单独的文件中,我们该如何停止 computed & watch 的响应式依赖呢?
在 Vue3.2 之前:
const disposables = []
const counter = ref(0)
const doubled = computed(() => counter.value * 2)
disposables.push(() => stop(doubled.effect))
const stopWatch1 = watchEffect(() => {
console.log(`counter: ${counter.value}`)
})
disposables.push(stopWatch1)
const stopWatch2 = watch(doubled, () => {
console.log(doubled.value)
})
disposables.push(stopWatch2)
EffectScope
实现:
// effect, computed, watch, watchEffect created inside the scope will be collected
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// to dispose all effects in the scope
scope.stop()
如何使用 #
一个 scope 可以执行一个 run 函数(接受一个函数作为参数,并返回该函数的返回值),并且捕获所有在该函数执行过程中创建的 effect ,包括可以创建 effect 的API,例如 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))
})
// the same scope can run multiple times
scope.run(() => {
watch(counter, () => {
/* ... */
})
})
当调用 scope.stop()
时,所有被捕获的 effect 都会被取消,包括 Nested Scopes 也会被递归取消。
嵌套 scope 也会被他们的父级 scope 收集。并且当父级 scope 销毁的时候,所有的后代 scope 也会被递归销毁。
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
// not need to get the stop handler, it will be collected by the outer scope
effectScope().run(() => {
watch(doubled, () => console.log(doubled.value))
})
watchEffect(() => console.log('Count: ', doubled.value))
})
// dispose all effects, including those in the nested scopes
scope.stop()
EffectScope
接受一个参数可以在分离模式(detached mode)下创建。 Detached Scope不会被父级收集。
let nestedScope
const parentScope = effectScope()
parentScope.run(() => {
const doubled = computed(() => counter.value * 2)
// with the detected flag,
// the scope will not be collected and disposed by the outer scope
nestedScope = effectScope(true /* detached */)
nestedScope.run(() => {
watch(doubled, () => console.log(doubled.value))
})
watchEffect(() => console.log('Count: ', doubled.value))
})
// disposes all effects, but not `nestedScope`
parentScope.stop()
// stop the nested scope only when appropriate
nestedScope.stop()
全局钩子函数 onScopeDispose
提供了类似于 onUnmounted
的功能,不同的是它工作在 scope
中而不是当前实例。
这使得 composable functions 可以通过他们的 scope
清除他们的副作用。
由于 setup()
默认会为当前实例创建一个 scope
,所以当没有明确声明一个 scope
的时候,onScopeDispose
等同于 onUnmounted
。
import { onScopeDispose } from 'vue'
const scope = effectScope()
scope.run(() => {
onScopeDispose(() => {
console.log('cleaned!')
})
})
scope.stop() // logs 'cleaned!'
通过 getCurrentScope()
可以获取当前 scope
import { getCurrentScope } from 'vue'
getCurrentScope() // EffectScope | undefined