先记住这个答案
effectScope 会建立一个响应式效果作用域;在 scope.run 的同步执行期间创建的 computed、watch 和 watchEffect 等效果会被它收集,调用 scope.stop 后可以一起停止。它适合组合式函数、临时工作区或独立响应式模块的集中销毁。作用域只管理被收集的响应式效果和登记的清理,并不会自动关闭未登记的定时器、连接或普通异步任务。作用域停止后再次 run 会返回 undefined,不能用它复活旧作用域。
- run 的同步执行期负责收集效果
- stop 统一结束已收集效果与清理
- 停止后的作用域不能重新启用
收集的是创建动作而非任意引用
一个 watch 在 scope.run 之外已经创建完成,后来即使把停止句柄放进作用域变量,也不会因此自动归属该作用域。关键是响应式效果创建时,哪个作用域处于活动状态。
同样地,run 回调里安排一个定时器,定时器稍后才创建 watch,这个创建动作已经越过同步收集窗口。需要延迟创建时应重新设计所有权,或者在正确的活动作用域中显式执行并保存停止路径。
用一次 stop 结束一组观察
下例在一个作用域中创建 watchEffect 和 watch。源变化后两者都留下记录,停止作用域再修改源,记录不再增长。调用方只需要保管 scope,无需逐个暴露内部句柄。
computed 也可以被作用域收集,但它仍保持按需取值的语义。作用域回答的是生命周期归属,不能把派生值的惰性计算、监听调度和新旧值模型合并成同一个概念。
import { effectScope, ref, watch, watchEffect } from 'vue'
const count = ref(0)
const events: string[] = []
const scope = effectScope()
scope.run(() => {
watchEffect(() => { events.push('effect:' + count.value) })
watch(count, value => { events.push('watch:' + value) })
})将 count 改为一并等待更新后,记录包含初次 effect 以及两种观察的一次更新。调用 scope.stop,再改为二并等待,记录保持不变;这验证的是统一停止,不承诺两个回调之间的所有内部排序。
停止以后要新建作用域
scope.stop 表示这批效果的所有权已经结束。需要重新开始时,应创建新的 effectScope 并重新执行初始化函数,而不是在旧 scope 上再次 run 或假定旧 computed 会重新订阅。
可重启模块应把创建过程封装为 start,把当前作用域保存在单一位置;再次 start 前先 stop 旧作用域,并清楚重置哪些业务状态。是否保留上次数据与作用域是否仍活跃是两个独立决定。
容易答错的地方
- 认为 run 中所有异步后续都会被收集
- run 只在同步执行回调时提供当前作用域。Promise 或定时器恢复后才创建的响应式效果需要自己的所有权安排,不能由代码缩进位置推断它仍属于原 scope。
- 只停止 watcher 不清理外部资源
- 观察回调创建的订阅、连接或定时器若没有登记清理,停止响应式效果也无法知道怎样关闭它们。资源建立时就用 onCleanup 或 onScopeDispose 接入结束流程。
面试官还会怎么问?
scope.run 的返回值有什么用途?
活动作用域会返回回调自己的结果,可用于把作用域内创建的状态和操作暴露给调用方。作用域已经停止时 run 返回 undefined,因此调用者不能无条件断言初始化结果存在。
组件 setup 中还需要手动创建 effectScope 吗?
组件本身已经为同步创建的响应式效果提供作用域,普通组件逻辑通常无需再包一层。需要把一组效果提前重置、独立于局部流程集中管理时,显式作用域才更有价值。
如何验证没有漏掉异步创建的监听?
记录每份效果的创建、停止和回调次数,在 stop 后继续修改所有测试源。再让延迟创建发生在 stop 之后,确认代码会拒绝创建或把新效果交给另一个明确所有者。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。