在 Vue 的 setup 中,响应会在开始初始化的时候被收集,在实例被卸载的时候,响应就会自动的被取消追踪了,这时一个很方便的特性。
但是,当我们在组件外使用或者编写一个独立的包时,这会变得非常麻烦。当在单独的文件中,我们该如何停止 computed & watch 的响应式依赖呢?
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()
该特性就是试图将组件的 setup() 响应式依赖收集和处理功能抽象为更通用的 API,该 API 可以在组件模型之外复用。
使用 effectScope() 创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。
使用 getCurrentScope() 返回当前活跃的 effect 作用域。
使用 onScopeDispose() 方法可以在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的。
鼠标跟踪器示例
使用组合式 API 实现鼠标跟踪功能,它会是这样的:
// mouse.js
import { ref, onMounted, onUnmounted } from 'vue'
// 按照惯例,组合式函数名以“use”开头
export function useMouse() {
// 被组合式函数封装和管理的状态
const x = ref(0)
const y = ref(0)
// 组合式函数可以随时更改其状态。
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
// 一个组合式函数也可以挂靠在所属组件的生命周期上
// 来启动和卸载副作用
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
// 通过返回值暴露所管理的状态
return { x, y }
}
如果在多个组件中调用 useMouse () ,则每个组件将附加一个 mouseemove 监听器。我们应该能够通过在多个组件之间共享相同的侦听器集和 refs 来提高效率,但是我们做不到,因为每个 onUnmounted 调用都耦合到一个组件实例。
我们可以使用分离作用域和 onScopeDispose 来实现这一点, 首先,我们需要用 onScopeDispose 替换 onUnmounted
- onUnmounted(() => {
+ onScopeDispose(() => {
window.removeEventListener('mousemove', handler)
})
这仍然有效,因为 Vue 组件现在也在作用域内运行其 setup () ,该作用域将在组件卸载时释放。
然后,我们可以创建一个工具函数来管理父范围订阅:
function createSharedComposable(composable) {
let subscribers = 0
let state, scope
const dispose = () => {
if (scope && --subscribers <= 0) {
scope.stop()
state = scope = null
}
}
// 这里只有在第一次运行的时候创建一个state, 后面所有的组件就不会再创建新的state,而是共用一个state
return (...args) => {
subscribers++
if (!state) {
scope = effectScope(true)
state = scope.run(() => composable(...args))
}
onScopeDispose(dispose)
return state
}
}
现在我们就可以使用这个 shared 版本的 useMouse
const useSharedMouse = createSharedComposable(useMouse)
通过这个例子,不禁想到,是否可以通过这种模式模拟 vuex 的能力?我们是否可以通过 shared composables 更加灵活的达到全局状态管理的目的呢?
export const useGlobalState = createSharedComposable(
() => {
const count = ref(0)
return { count }
}
)
VueUse
createSharedComposable 方法 VueUse 库也提供了,让我们看看源码。
import { getCurrentScope, onScopeDispose, effectScope } from 'vue'
export function createSharedComposable(composable) {
let subscribers = 0
let state
let scope
const dispose = () => {
subscribers -= 1
if (scope && subscribers <= 0) {
// 取消追踪
scope.stop()
state = undefined
scope = undefined
}
}
return ((...args) => {
subscribers += 1
if (!state) {
// 创建一个 effect 作用域,可以捕获其中所创建的响应式副作用 (即计算属性和侦听器),这样捕获到的副作用可以一起处理。
scope = effectScope(true)
state = scope.run(() => composable(...args))
}
tryOnScopeDispose(dispose)
return state
})
}
function tryOnScopeDispose(fn) {
// 返回当前活跃的 effect 作用域。
if (getCurrentScope()) {
// 在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
onScopeDispose(fn)
return true
}
return false
}