先记住这个答案
onScopeDispose 把回调登记到当前活动 effect scope;对应作用域 stop 时执行。组件 setup 本身运行在效果作用域中,所以组合式函数可以用它释放定时器、全局事件订阅、外部观察器或连接,而不必与组件 API 强耦合。没有活动作用域时调用会警告;Vue 3.5 起可用第二个 true 参数抑制该警告,但静默不会凭空创建清理所有者。资源的建立与登记应紧邻,并保证关闭函数可重复安全调用。
- 清理绑定当前活动 effect scope
- 组件 setup 也提供可用作用域
- 无活动作用域时必须另有释放策略
它让 composable 脱离组件名称
如果可复用函数只使用 onUnmounted,它暗示自己必须由组件直接拥有。改用 onScopeDispose 后,同一函数既能在组件 setup 中使用,也能在显式 effectScope 中使用,结束责任由当前 owner 决定。
这种抽象仍依赖调用时存在活动 scope。模块顶层或普通事件函数里调用可能没有 owner,库函数应通过 getCurrentScope 检查并报出清楚错误,或者返回 dispose 让调用方手动管理。
把订阅与退订放在一起
示例模拟事件源,subscribe 返回取消函数。组合式函数创建订阅后立刻登记取消,调用方停止 scope 就能让监听数量归零,不需要知道内部使用了哪个事件系统。
真实资源的关闭可能抛错或需要异步完成。清理入口应保证本地引用及时失效,并按项目策略记录错误;不要让一个资源的关闭失败阻断其他必要清理,也不要把异步清理误称为 scope.stop 会等待完成。
import { effectScope, onScopeDispose } from 'vue'
function useSubscription(subscribe: (listener: () => void) => () => void) {
const unsubscribe = subscribe(() => {})
onScopeDispose(unsubscribe)
}
const listeners = new Set<() => void>()
const scope = effectScope()
scope.run(() => useSubscription(listener => {
listeners.add(listener)
return () => listeners.delete(listener)
}))初始化后 listeners.size 为一,scope.stop 后为零。该例只验证登记和释放,不依赖浏览器事件;生产实现应让 unsubscribe 即使被多条结束路径调用也保持安全。
无作用域时不要悄悄假装成功
Vue 3.5 的 failSilently 参数适合库函数在可选 scope 环境中探测使用,避免开发警告,但它不会保存回调。若资源已经创建,静默跳过登记就可能泄漏,因此必须同时返回手动 dispose 或选择不创建。
一个稳妥模式是先检查 getCurrentScope,再建立资源。若设计允许无 scope,就把 dispose 明确交给调用者;若不允许,则在创建前抛出可理解的错误,而不是先连接后发现无人负责关闭。
容易答错的地方
- 在资源创建很久后才登记清理
- 中间若抛错或作用域已经结束,资源就可能失去回收路径。先取得取消函数便立即登记;异步创建资源时还要处理等待期间 owner 已结束的竞态。
- 把 true 参数理解为自动创建作用域
- failSilently 只控制没有活动 scope 时是否警告,不会建立 owner,也不会在未来自动运行回调。静默模式必须配套另一条明确的手动释放路径。
面试官还会怎么问?
它和 watch 回调里的 onCleanup 有什么区别?
onCleanup 管理某个 watcher 每一轮副作用,在下次重跑或停止该 watcher 时触发;onScopeDispose 管理整个当前 scope 的寿命。一个组合式函数可能同时使用两者处理不同层级资源。
清理函数能写成 async 吗?
可以启动异步工作,但 stop 本身不应被当作等待所有 Promise 完成的事务。若退出流程必须确认资源已关闭,应另外提供可 await 的 close,并让作用域清理作为兜底触发。
组件中还需要同时写 onUnmounted 吗?
同一资源通常选择一个明确所有者即可。setup 当前作用域会随组件结束,onScopeDispose 已能覆盖;重复登记同一关闭动作可能造成二次调用,因此外部关闭函数仍应幂等。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。