watchEffect 的清除回调通过 onCleanup 注册,会在副作用重新执行前或组件卸载时自动调用,用于处理接口竞态等清理任务。

watchEffect 的清除回调通过 onCleanup 函数注册,它会在副作用函数重新执行前、或组件卸载时自动调用,是处理接口竞态(如多次搜索请求只保留最后一次)的核心机制。
onCleanup 的基本写法
在 watchEffect 的回调函数中,直接调用传入的 onCleanup 参数(它是一个函数),把需要清理的逻辑(比如取消请求、清除定时器)写在里面:
- onCleanup 必须在 watchEffect 回调内部调用,不能异步延迟调用(如放在 setTimeout 里)
- 每次 watchEffect 重新运行前,上一次注册的 onCleanup 会先执行
- 组件 unmount 时,最后一次注册的 onCleanup 也会执行
用 onCleanup 取消 axios 请求(竞态控制)
axios 自带 CancelToken 或 AbortController 支持。推荐用 AbortController(现代、标准、无需额外 polyfill):
const controller = new AbortController();
watchEffect((onCleanup) => {
// 每次触发都发起新请求,自动 abort 上一次
api.search(query.value, { signal: controller.signal })
.then(data => result.value = data)
.catch(err => {
if (err.name !== 'AbortError') {
console.error('请求失败:', err);
}
});
// 注册清理:触发时 abort 当前请求
onCleanup(() => {
controller.abort();
});
});
这样,当 query 变化或组件销毁,上一个请求就被取消,不会出现旧响应覆盖新状态的问题。
手动管理请求标识(不依赖 AbortController)
如果接口不支持 signal(比如某些封装的 request 方法),可用简单计数或标记位控制:
- 定义一个响应式 ref(如 pendingId),每次请求前递增并记录
- 在 then/catch 中判断当前响应是否匹配最新的 pendingId
- onCleanup 不直接取消请求,而是更新标记,让回调自行忽略过期响应
示例:
let pendingId = 0;
watchEffect((onCleanup) => {
const id = ++pendingId;
api.search(query.value).then(data => {
if (id === pendingId) { // 仅处理最新请求的响应
result.value = data;
}
});
onCleanup(() => {
// 无需操作,仅靠 id 判断即可
});
});
常见误区提醒
onCleanup 不是“必须写”,但凡有异步、定时器、事件监听等长期任务,就必须清理,否则易导致内存泄漏或状态错乱:
- 写了 setTimeout/setInterval 却没 clearTimeout/clearInterval → 内存泄漏 + 多余执行
- 添加了 window.addEventListener 却没 removeEventListener → 重复绑定 + 错误触发
- 在 onCleanup 里又发起异步操作(如再调一次 api)→ 无法保证执行时机,不推荐
竞态问题本质是“旧任务不该影响新状态”,onCleanup 提供的是“任务退出通道”,配合请求取消或响应过滤,就能稳稳守住数据一致性。

















