先记住这个答案
watch 和 watchEffect 都返回可调用的句柄,调用它即可停止该侦听器;当前 API 也提供句柄的 stop 方法。后续源变化不再让这个侦听器重新执行,已注册的副作用清理也会在停止时运行。但 stop 不会撤销之前写入的状态,也不能凭空中断已经开始的 Promise、网络请求或定时器。需要把取消或失效逻辑注册到清理回调里。
- 调用返回句柄即可停止侦听
- 停止不会回滚已发生的副作用
- 异步任务需要自己的取消或失效逻辑
停止句柄对应一个侦听器
一次调用 watch 得到一个句柄,它只管理这次创建的侦听器。相同源上另外注册的侦听器仍然有效,源本身也保持响应式,不会因为其中一个订阅被停止就变成普通变量。
示例先让副作用执行初值,再等待一次更新刷新,随后停止并继续修改源。记录中没有停止后的值,这样可以把队列尚未刷新和订阅已经失效两种情况明确区分开。
import { ref, watchEffect, nextTick } from 'vue'
const count = ref(0)
const seen: number[] = []
let cleanups = 0
const stop = watchEffect((onCleanup) => {
seen.push(count.value)
onCleanup(() => { cleanups++ })
})
count.value = 1
await nextTick()
stop()
count.value = 2
await nextTick()
console.log(seen, cleanups)在默认调度方式下,seen 依次记录零和一,清理执行两次:重新执行前一次、手动停止时一次。最后赋值为二没有再次运行副作用。代码可放进支持顶层 await 的模块实验。
提前结束观察的实际场景
例如一次上传完成后不再需要跟踪进度,页面却仍然保留成功结果,此时可以提前停止对应侦听器。把句柄保存在这次上传控制对象里,比依赖页面最终卸载更贴合任务的真实结束时间。
若侦听器在运行时创建了订阅、请求或定时器,应在本轮回调中登记清理。停止观察和关闭外部连接是两件事;只有清理函数实际调用取消接口,外部资源才会按其自身协议结束。
停止后的状态与重新开始
被停止的侦听器不会因为源再次变化自动复活,也不会把界面还原为最初状态。需要重新观察时创建一个新的侦听器,并显式决定是否立即执行初始逻辑,避免把旧句柄当成启动开关。
如果先启动异步操作,再在它返回之前停止,异步函数仍可能继续运行。使用本轮有效标记检查提交资格,或者使用支持取消的接口;尤其不能只停止订阅却让旧结果写回已结束的任务状态。
容易答错的地方
- 以为停止等于清空状态
- 停止控制的是后续订阅行为,已写入的列表、错误提示和缓存不会被自动删除。需要重置界面时,应由业务结束流程明确赋初值,并考虑是否要保留完成结果。
- 停止后才寻找资源句柄
- 取消函数若只保存在异步回调深处,停止时可能无法及时取得它。资源一创建就登记清理,必要时同时记录有效标记,使停止和异步完成之间的竞争有确定的处理结果。
面试官还会怎么问?
同一个源上的其他 watch 会一起停止吗?
不会。每次创建的侦听器分别持有自己的订阅和清理逻辑。若多个监听要共同结束,可以把句柄收集起来统一调用,或者将它们放入有明确所有者的作用域中管理。
重复调用停止函数需要额外做保护吗?
Vue 的停止操作可重复调用,不应因此把同一轮清理再次执行。业务的取消函数仍建议具备重复调用安全性,因为它也可能由用户取消按钮或其他结束路径直接触发。
已经排入队列但尚未执行的回调怎么办?
在侦听器真正执行前调用 stop,会让已停止的效果失去继续运行资格。应通过等待更新前后分别观察来验证这一边界,不要把源码里的任务入队等同于回调一定会执行。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。