先记住这个答案
onMounted 等 API 需要知道当前活动组件实例,才能把回调注册到对应生命周期。组合式函数被 setup 同步调用时通常具备这个上下文;放进普通定时器或任意异步回调后再注册,可能失去实例关联。同步注册不代表回调本身不能处理异步工作。script setup 的受支持顶层 await 有编译器恢复上下文的特殊处理,不能据此认为所有 async 函数中的 await 后都能任意注册钩子。
- 注册时的实例上下文决定钩子归属
- 先同步注册,再在回调中启动需要的工作
- script setup 的编译支持有边界,普通异步函数不自动继承
同步调用如何保留组件归属
组件 setup 中调用一个普通函数,该函数再调用 onMounted,仍处于同步调用链里,Vue 可以把它关联到当前组件。若先等定时器触发再执行 onMounted,原来的 setup 早已返回,系统不能仅凭闭包中的变量推断应该挂在哪个实例上。
因此封装时通常先注册需要的生命周期,再把实际工作放进回调。下面把订阅的启动和停止放到挂载与卸载阶段,组合式函数本身在 setup 同步执行。subscribe 的契约是同步返回清理函数,避免订阅已创建却无法明确释放。
import { onMounted, onUnmounted } from 'vue';
function useSubscription(subscribe: () => () => void) {
let stop: (() => void) | undefined;
onMounted(() => { stop = subscribe(); });
onUnmounted(() => { stop?.(); });
}把 useSubscription 放在组件 setup 的同步调用链中,挂载后启动一次订阅,卸载时调用返回的停止函数。该辅助函数本身没有使用 DOM,因此可以用最小组件宿主验证生命周期。
await 的位置为何重要
onMounted(async () => { ... }) 仍然可以同步完成注册,异步的是之后执行的回调。相反,在普通 async 组合式函数里先 await 再调用 onMounted,往往已经越过有效实例上下文。两者表面都出现 async,生命周期关联却不同。
script setup 对特定顶层 await 会生成恢复上下文的代码,所以其后调用组合式函数有专门支持。这属于编译语境,不会自动扩展到任意外部异步函数的内部 await。阅读旧代码时应先确认语法所在位置和编译器处理,避免套用一句 await 后永远不行或永远没问题。
注册成功还要保证资源能结束
钩子可以正确注册,但异步任务仍可能在组件卸载后完成并写入过期状态。需要根据任务设计取消、失效标识或显式释放,不能以 onUnmounted 存在就推断所有异步后续都已停止。共享资源还要避免一个组件卸载时错误关闭其他使用者的连接。
SSR 中 mounted 和 unmounted 等客户端生命周期不按浏览器方式执行,依赖 DOM 的代码应放在合适的客户端阶段。不要在服务端 setup 启动必须靠 unmounted 停止的计时器,再假定请求结束会自动清理。生命周期方案要同时考虑运行环境和资源所有者。
容易答错的地方
- 同步注册表示回调不能是 async
- 注册过程与回调内部工作不同,关键是登记时上下文正确,并处理异步任务的结束与失效。排查“Vue 组合式函数生命周期 setup 同步注册与 await”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
- 所有 await 后都会自动恢复当前实例
- 这种支持来自特定 script setup 编译处理,普通异步回调不能直接套用。排查“Vue 组合式函数生命周期 setup 同步注册与 await”时还要核对输入、版本和执行顺序,并用反例确认修复后的边界。
面试官还会怎么问?
纯 computed 辅助函数也必须挂载组件才能用吗?
不一定。没有依赖生命周期或注入上下文的纯响应式逻辑可以独立使用,但创建的副作用仍要有明确作用域。
能把 onMounted 放在点击事件里吗?
通常不应这样注册挂载钩子,点击时组件已经挂载,且不是最初的 setup 注册阶段;应直接执行点击对应动作。
组件卸载后 Promise 一定停止吗?
不会。Promise 及外部任务需要相应取消或过期结果保护,生命周期清理只是触发这些措施的时机。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。