先记住这个答案
可以为共享服务建立 detached effectScope,在第一个消费者到来时创建一次 watch 或全局订阅,后续消费者复用同一状态;每个消费者离开时减少引用,归零后 stop scope 并清空实例。release 必须幂等,下一次消费者到来要新建 scope。浏览器应用可按 app 创建容器,SSR 则应按请求或应用实例隔离,不能把带用户信息的可变单例放在模块顶层跨请求复用。
- 首个消费者启动,后续消费者复用
- 最后消费者释放时停止 detached scope
- 服务端共享状态必须按请求或应用隔离
先确定共享键和容器边界
如果一个组件要跟踪窗口尺寸,多个消费者通常可以共用同一份浏览器监听。如果函数参数包含房间编号或用户身份,则不能把所有调用都塞进同一个实例,应按稳定键保存多个服务并分别计数。
SSR 中应由每次 createApp 或请求上下文持有这张实例表。模块顶层变量在长驻进程里可能服务后续请求,使一个用户的状态被另一个请求读到;这不是 effectScope 能自动解决的隔离问题。
用幂等 release 结束最后一份引用
下例展示单键共享容器的核心寿命逻辑。第一次 acquire 创建 detached scope 并在其中建立效果,第二次只增加计数;每个返回的 release 用局部布尔值保证最多减少一次。
归零时先保存并清除全局当前引用,再停止旧 scope,可以降低清理回调重入时误拿旧实例的风险。生产代码还应处理初始化抛错:只有创建成功后才增加可对外释放的引用。
import { effectScope, ref, watchEffect } from 'vue'
const source = ref(0)
let current: { scope: ReturnType<typeof effectScope>, users: number, seen: number[] } | undefined
function acquireShared() {
if (!current) {
const scope = effectScope(true)
const seen: number[] = []
scope.run(() => watchEffect(() => seen.push(source.value)))
current = { scope, users: 0, seen }
}
const service = current
service.users++
let released = false
return { service, release() {
if (released) return
released = true
service.users--
if (service.users === 0 && current === service) {
current = undefined
service.scope.stop()
}
}}
}两个消费者会拿到同一 service;释放第一个后效果仍活动,第二个释放后 scope 停止。重复调用同一 release 不会让计数为负,下一次 acquire 会建立全新实例。
与组件生命周期正确衔接
组件 setup 中 acquire 后,可以通过 onScopeDispose 登记这次消费者的 release。共享 scope 自身保持 detached,不会因为任意一个组件先卸载就结束;只有所有登记的消费者都释放才关闭。
还要考虑 KeepAlive:失活是否释放取决于服务是否应在缓存期间继续工作。如果只在可见时需要,应在 activated 与 deactivated 管理订阅;若缓存组件仍算活跃消费者,则让最终卸载负责释放。
容易答错的地方
- 每次调用都创建 detached scope
- 这样虽然每个组件都能工作,却没有实现共享,且一旦漏掉停止就会累积全局监听。应将实例保存在明确容器中,只让首次 acquire 负责创建。
- release 非幂等导致提前关闭
- 组件清理、错误回退和手动关闭可能走多条路径。若同一消费者重复减计数,其他仍在线的组件会突然失去服务;局部 released 标记能封住这类重复释放。
面试官还会怎么问?
不同参数的共享服务如何管理?
用规范化后的参数作为键,Map 中每个键保存独立 scope、状态和引用数。release 时只修改相应条目,归零停止并删除;对象键还要明确身份语义和回收策略。
为什么不用组件 provide 和 inject 就够了?
provide 可以确定容器边界并分发服务,很适合每个 app 或子树一份实例;effectScope 继续负责服务内部多个响应式效果的统一停止。两者解决分发与生命周期两个层面,可以组合使用。
共享效果停止后状态要保留吗?
按业务决定。若下次进入需要新鲜订阅,清除实例最清楚;若要保留只读缓存,可把缓存与活动 scope 分开保存,但必须避免旧缓存带用户身份跨请求泄漏。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。