先记住这个答案
onWatcherCleanup 是 Vue 3.5 新增的清理注册 API,必须在 watch 回调或 watchEffect 函数同步执行期间调用,不能放到 await 之后。参数形式的 onCleanup 绑定到侦听器实例,不受相同的同步上下文限制;它在 watch 中是第三个参数,在 watchEffect 中是第一个参数,并未因此被弃用。两者都建议在启动资源后尽早注册,避免清理点先于注册到达。
- onWatcherCleanup 需要 Vue 3.5 及同步上下文
- 参数 onCleanup 仍然有效且绑定侦听器
- 允许晚注册不代表可以忽略失效竞态
同步限制来自当前执行上下文
导入一个函数不会让它永久记住某个侦听器。onWatcherCleanup 需要在注册当时定位当前活动的观察执行过程,普通 await 之后的继续执行已经脱离这个同步窗口,所以不能再依赖它关联清理。
参数式 onCleanup 则由侦听器传入,已经带有与该实例的关联。抽取工具函数时,可以选择同步调用 onWatcherCleanup,也可以显式传入 onCleanup,让依赖关系从函数签名中就能看出来。
把清理责任传给资源辅助函数
示例在立即执行的 watch 中创建一项可释放资源,并让辅助函数同步登记释放动作。业务回调只处理资源的用途,而资源创建与释放紧挨着,减少后续重构遗漏清理的机会。
示例使用参数式接口,便于兼容未升级到三点五的项目。选择新接口时先确认运行时版本,不应只因为编辑器能识别类型就假定部署环境已经提供同名导出。
import { ref, watch } from 'vue'
type RegisterCleanup = (cleanup: () => void) => void
function attachResource(key: string, register: RegisterCleanup, events: string[]) {
events.push('open:' + key)
register(() => { events.push('close:' + key) })
}
const key = ref('A')
const events: string[] = []
const stop = watch(key, (value, _old, onCleanup) => {
attachResource(value, onCleanup, events)
}, { immediate: true })切换 key 并等待刷新时会先关闭旧资源再打开新资源,stop 则关闭当前资源。示例用事件记录代替真实连接,验证的是登记与执行顺序,不会建立外部网络连接。
为什么仍然不推荐等待后才登记
即使参数式接口允许在 await 后调用,旧任务也可能在等待期间已经失效。此时资源创建完成后需要先检查本轮资格;若已经结束,应立刻释放,而不是以为补登记就会自动补做过去的清理。
最容易审查的顺序是建立取消控制、登记清理、再等待结果。遇到必须异步创建才能取得释放函数的 SDK,则把创建中的取消与创建后的释放分别管理,并覆盖停止发生在两者之间的测试。
容易答错的地方
- 把第三参数当成弃用接口
- 新接口增加了一种组织清理代码的方式,没有宣布参数式 onCleanup 失效。盲目替换会把原本可显式传递的清理能力变成对活动上下文的隐式依赖,还可能引入版本兼容问题。
- 在 await 后补 onWatcherCleanup
- 这时通常已无可关联的活动侦听器,开发环境会给出警告,清理也不会按期待登记。应把注册移动到同步部分,不能通过忽略警告来让上下文关联重新出现。
面试官还会怎么问?
watchEffect 的清理参数也是第三个吗?
不是。watchEffect 接收的效果函数第一个参数就是 onCleanup;watch 回调已有新值和旧值,因此清理函数位于第三个位置。两种签名不要混写,否则取得的可能根本不是注册函数。
辅助函数内部能调用 onWatcherCleanup 吗?
可以,只要辅助函数是在侦听器同步执行过程中同步调用的,期间没有越过异步边界。若辅助函数可能独立使用,显式接受清理注册函数通常更便于检查它依赖谁来管理资源。
为什么注册越晚越容易出现泄漏?
资源创建和登记之间若存在等待,侦听器可能先重跑或停止。释放责任尚未交给清理流程,旧资源就有机会遗留;因此要缩短这段窗口,必要时在创建完成后对失效状态做一次即时释放。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。