在 Vue 3 的组合式 API 中,我们经常使用 watch、watchEffect、computed 等来创建副作用。这些副作用默认会在组件销毁时自动停止,避免内存泄漏。但在一些特殊场景下,我们希望在组件外部或某个独立的逻辑单元内统一管理副作用的生命周期,而不是等整个组件卸载时才清理。
这就是 effectScope 的作用:手动创建一个“作用域”,在其中声明的所有响应式副作用(watch、watchEffect、computed 等)会在该作用域被销毁时一键停止。
典型场景:在组件 setup 外使用组合式函数
假设我们封装了一个 useEventListener 组合式函数,它在模块级别使用,而不是在组件的 setup 里:
// 危险示例:副作用永远不会自动停止
const listenerScope = null;
function useEventListener(target, event, handler) {
watchEffect((onCleanup) => {
target.addEventListener(event, handler);
onCleanup(() => target.removeEventListener(event, handler));
});
}
// 如果在模块顶层直接调用,副作用将永远运行
useEventListener(window, 'resize', () => {});
上面的代码中,因为 watchEffect 没有在组件的 setup 中运行,Vue 无法自动收集停止函数,副作用会一直保留,造成内存泄漏。为了解决这个问题,我们可以手动创建一个 effectScope,并在这个作用域内使用组合式函数:
import { effectScope } from 'vue';
const scope = effectScope();
scope.run(() => {
// 这个 watchEffect 现在属于 scope
watchEffect((onCleanup) => {
window.addEventListener('resize', handler);
onCleanup(() => window.removeEventListener('resize', handler));
});
// 可以同时有多个副作用
watch(source, callback);
});
// 在合适的时机统一销毁
scope.stop(); // 所有 run 内创建的副作用都会停止
在组合式函数内部创建独立作用域
更常见的用法是编写可复用的组合式函数时,内部使用 effectScope 来管理自身内部的所有副作用,并暴露一个停止函数。这样调用者可以精确控制副作用的生命周期:
// useMouseTracker.js
import { effectScope, ref, onScopeDispose } from 'vue';
export function useMouseTracker() {
const x = ref(0);
const y = ref(0);
const scope = effectScope();
scope.run(() => {
const onMouseMove = (e) => {
x.value = e.clientX;
y.value = e.clientY;
};
window.addEventListener('mousemove', onMouseMove);
// 当作用域停止时自动移除事件
onScopeDispose(() => {
window.removeEventListener('mousemove', onMouseMove);
});
});
// 返回数据与停止函数
return {
x,
y,
stop: () => scope.stop()
};
}
// 在组件中使用
const { x, y, stop } = useMouseTracker();
// 组件销毁时 Vue 会自动 stop,但你也可以手动提前停止
// stop();
嵌套作用域与 onScopeDispose
effectScope 返回的对象上有一个 run 方法,它会创建一个“子作用域”。如果我们在 run 内部再次调用 effectScope(),会得到当前活动作用域的子域,父域停止时会同时停止所有子域。
onScopeDispose 是一个钩子,用于在当前活跃的作用域停止时执行清理工作。如果没有活跃作用域(例如在组件外直接调用),它会回退到组件的 onUnmounted 钩子(前提是在 setup 中)。
const parentScope = effectScope();
parentScope.run(() => {
const childScope = effectScope(); // 子作用域
childScope.run(() => {
watchEffect(() => { /* ... */ });
});
// 停止父作用域,子作用域也会被停止
});
parentScope.stop();
与组件生命周期的关系
在组件的 setup 函数中,Vue 默认会为每一个组件实例创建一个“组件作用域”,所有在组件 setup 中创建的副作用(watch、watchEffect 等)都会自动收集到该作用域中,组件卸载时自动停止。所以你不需要在普通组件中手动使用 effectScope。
effectScope 真正发力的场景是:
- 在组件外部(Node.js 环境、全局工具函数)创建可清理的响应式逻辑。
- 编写不需要组件的独立响应式系统(如状态机、自动保存模块)。
- 在大型组合式函数中,为了逻辑清晰,主动管理内部副作用群的生命周期。
注意点
effectScope是 Vue 3.2+ 提供的 API。- 在
scope.run()中创建的computed、watch、watchEffect都会绑定到该作用域。 - 调用
scope.stop()后,所有子作用域和副作用都会停止,且不可恢复。 - 在
scope.run()中返回的值可以直接在外部使用,作用域停止后,这些值只是不再自动更新,但数据本身仍然可读(只是失去了响应式依赖的驱动)。
总结一句:effectScope 让你拥有“手动管理副作用群”的能力,非常适合用来封装独立于组件生命周期的响应式逻辑,是构建高级组合式函数的利器。