Vue3 Effect Scope 完全指南:onScopeDispose 与 effectScope 的深度使用
Vue3 Effect Scope 完全指南onScopeDispose 与 effectScope 的深度使用概述Vue 3.2 引入了 Effect Scope API提供了一种**集中管理和自动清理副作用effect**的机制。如果你写过组合式函数Composables却担心忘记清理watch或定时器那这套 API 正是你需要的。核心 API 包括effectScope()— 创建一个作用域其内的所有响应式副作用watch、computed、watchEffect 等可被统一停用onScopeDispose()— 注册一个回调当所在的作用域被销毁时触发类似onUnmounted但可用于非组件场景getCurrentScope()— 获取当前活跃的作用域实例需要留意的是本文内容已对照 Vue 官方最新文档进行逐条校验文中标注的⚠️ 非公开 API在实践中非常重要——不要踩坑。一、为什么需要 Effect Scope组件内的问题在 Vue 组件中副作用生命周期由组件自身管理script setupimport{onMounted, onUnmounted, watch, ref}fromvueconst countref(0)const stopwatch(count,(val){console.log(count changed:, val)})onMounted((){ const timersetInterval((){/*...*/},1000)onUnmounted(()clearInterval(timer))})/script组件卸载时Vue 会自动清理组件内创建的watch、computed等副作用。非组件场景的问题但在以下场景没有「组件卸载」这个生命周期事件 封装可复用的组合式函数Composables 插件或库的内部逻辑 在组件外部使用响应式 API 动态创建/销毁的临时作用域手动清理这些副作用容易遗漏导致内存泄漏。二、核心 API2.1effectScope()创建一个 Effect Scope返回一个scope对象。// 函数签名functioneffectScope(detached?: boolean): EffectScope // 公开类型Vue 官方 API 仅暴露这两个方法 interface EffectScope{runT(fn:()T): T|undefined // 在作用域内运行函数scope 非活跃时返回 undefined stop(): void // 停止作用域清理所有内部副作用}基本用法import{effectScope, watch, ref, computed}fromvueconst scopeeffectScope()const countref(0)// 在作用域内创建的所有效果会被 scope 收集 scope.run((){const doubledcomputed(()count.value *2)watch(doubled,(val){console.log(doubled:, val)})watch(count,(val){console.log(count:, val)})})// 统一清理所有副作用 scope.stop()// 此时computed 被销毁、watch 被停止、内存释放detached参数脱离父级作用域const parenteffectScope()const childeffectScope(true)// detachedtrue脱离父级 parent.run((){// 另一个嵌套 scope非 detached会随 parent 自动清理 effectScope().run((){/* 自动随 parent 清理 */})})parent.stop()// child 不受影响仍可继续使用 child.run((){/* 仍然有效 */})⚠️关于on()/off()/active等属性 源码内部EffectScope类确实包含这些方法但它们不是 Vue 官方公开的 API你只能在.d.ts类型声明中看到run和stop。不要在生产代码中依赖scope.on()/scope.off()/scope.active若需要暂停/恢复范围请自行封装或使用detached分离 scope 生命周期官方推荐的使用方式就是scope.run(fn)scope.stop()2.2onScopeDispose()注册一个回调函数在所在作用域被销毁时自动执行。import{onScopeDispose}fromvue// Vue3.5 完整签名functiononScopeDispose(fn:()void, failSilently?: boolean): void与onUnmounted的对比特性onUnmountedonScopeDispose使用场景仅在组件setup()内任何Effect Scope内触发时机组件卸载时所在 scope 调用stop()时非组件可用❌ 会报错✅ 可放心使用兼容组件—✅ 组件 scope 销毁时也会触发onScopeDispose是onUnmounted的超集在组件中onScopeDispose的效果等同于onUnmounted因为组件也有自己的 Effect Scope。Vue 3.5新增第二个参数failSilently当当前没有活跃的 Effect Scope 时代替抛出警告静默失败// 不确定是否在 scope 内不想抛警告 onScopeDispose(cleanup,true)使用场景封装 setIntervalimport{onScopeDispose}fromvue// 组合式函数自动清理定时器exportfunctionuseInterval(fn:()void, ms: number){const timersetInterval(fn, ms)// ✅ 不管是组件卸载还是 scope.stop()都能自动清理 onScopeDispose((){clearInterval(timer)})}在组件中使用script setupimport{useInterval}from./composables/useInterval// 组件卸载时 → onScopeDispose → clearInterval useInterval(()console.log(tick),1000)/script在非组件 scope 中使用const scopeeffectScope()scope.run((){useInterval(()console.log(tick),1000)})// 手动销毁 → onScopeDispose → clearInterval scope.stop()三、核心场景场景 1封装健壮的可组合函数// composables/useMousePosition.tsimport{ref, onScopeDispose}fromvueexportfunctionuseMousePosition(){const xref(0)const yref(0)const handler(e: MouseEvent){x.valuee.clientX y.valuee.clientY}window.addEventListener(mousemove, handler)// ✅ 不管谁调用这个 composablescope 销毁时自动清理 onScopeDispose((){window.removeEventListener(mousemove, handler)})return{x, y}}场景 2插件/库的隔离作用域functioncreatePlugin(){const scopeeffectScope()letcleanup:(()void)|nullnullreturn{install(app: App){scope.run((){const statereactive({/*... */})app.provide(pluginState, state)cleanup()scope.stop()})},uninstall(){cleanup?.()}}}场景 3getCurrentScope()判断执行环境import{getCurrentScope, onScopeDispose}fromvueexportfunctionuseFeature(){// 检查是否在 Effect Scope 中运行if(getCurrentScope()){onScopeDispose((){console.log(scope 被销毁了)})}else{console.warn(useFeature 需要在 setup 或 scope.run 中调用)}}四、嵌套 ScopeScope 支持嵌套子 Scope 会在父 Scope 销毁时自动销毁。const parenteffectScope()parent.run((){const childeffectScope()child.run((){// 一些副作用...})// parent.stop()时child 会自动被 stop // 等价于手动调用 child.stop()再 parent.stop()})⚠️ 嵌套 scope 需要在parent.run()内部创建才会被自动收集。五、最佳实践✅ 统一使用onScopeDispose替代onUnmounted// ❌ 限制在组件内import{onUnmounted}fromvue// ✅ 任何场景都能用import{onScopeDispose}fromvue✅ 组合式函数中所有清理逻辑集中管理exportfunctionuseWebSocket(url: string){const wsnew WebSocket(url)const messagesrefstring[]([])ws.onmessage(e)messages.value.push(e.data)onScopeDispose((){ws.close()})return{messages}}✅ 临时 scope 用完后记得stop()const scopeeffectScope()scope.run((){/*... */})// 不需要时务必调用 scope.stop()❌ 不要在scope.stop()后继续使用内部创建的响应式对象letdoubled: ComputedRefnumberscope.run((){doubledcomputed(()count.value *2)})scope.stop()// ❌ doubled.value 此时已无效六、与现有 Vue 机制对比机制适用场景清理方式非组件可用watch返回值.stop()单个 watch 的手动停止手动✅onUnmounted组件内清理自动组件卸载❌onScopeDispose任意 scope 内清理自动scope.stop✅effectScope().stop()批量管理副作用自动批量✅七、Vue 源码中的 Effect Scope在 Vue 内部每个组件实例都拥有自己的 Effect Scope// 源码简化示意functionsetupComponent(instance){const scopeeffectScope()// 组件级别的 scope instance.scopescope scope.run((){// setup()中所有的 watch/computed 都会被收集 const setupResultinstance.setup()//...})// 组件卸载时 instance.onUnmounted((){scope.stop()// 清理组件所有副作用})}这就是为什么在组件中watch、computed会自动随组件卸载清理——它们都被收集到了组件的 Effect Scope 中。八、一个常见坑on()/off()是非公开 API网上很多文章会展示这样的写法// ⚠️ 危险写法Vue 官方未公开 on()/ off()const scopeeffectScope()scope.on()// 恢复 scope.off()// 暂停实际上Vue 官方 API 文档中EffectScope接口只包含了run和stop两个方法。on()/off()是源码内部方法没有在公开类型中暴露。虽然在当前版本中你调用它不会报错但是依赖内部 API 可能导致未来版本升级后功能失效请勿在生产代码中使用。如果你需要真正的暂停/恢复语义可以使用detached参数 重新创建 scope 的策略。总结概念一句话effectScope()创建一个副作用的「容器」方便统一管理onScopeDispose()在容器销毁时自动执行清理逻辑组件场景组件本身就是一个 scopeonScopeDisposeonUnmounted非组件场景手动创建 scope手动调用stop()清理嵌套 scope子 scope 随父 scope 自动销毁Effect Scope 的核心价值让响应式副作用的生命周期不再依赖组件从而写出真正可复用的、安全的组合式函数。 如果你正在写组合式函数现在就可以用onScopeDispose替换onUnmounted——所有清理逻辑都会变得更健壮。如果你在封装插件或需要动态管理副作用effectScope是你最好的朋友。本文内容已对照 Vue.js 官方文档2026-07-08逐条校验如有疏漏欢迎指正。如果本文对你有所帮助欢迎点赞 收藏 ⭐ 关注 后续会继续分享 Vue3 进阶内容。