先记住这个答案
onMounted、onUnmounted 等组合式生命周期 API 应在 setup 的同步执行窗口注册。Vue 执行 setup 时会设置当前组件实例,注册函数据此把回调挂到该实例;setup 返回后,普通 Promise、定时器或请求回调再调用钩子时通常已经没有当前实例,开发环境会警告,回调也不会补挂。正确做法是同步注册钩子,在回调内部根据异步结果或条件决定工作。需要单独说明:单文件组件的 <script setup> 顶层 await 会由编译器生成恢复当前实例上下文的代码,不能和任意异步回调混为一谈。
- 同步 setup 提供明确的当前实例
- 异步条件应放进已注册的回调内部
- script setup 顶层 await 存在编译器特例
迟到注册丢失的是归属关系
组件可以并发创建、更新和销毁,框架不能根据一个稍后到达的 Promise 回调安全猜测它原来属于谁。Vue 因而只在 setup 正在执行时暴露明确的活动实例,让依赖注入、生命周期和作用域清理绑定到正确 owner。
同样的代码偶尔看似可用,通常是时序巧合,不应依赖。开发版警告是直接证据;生产版可能省略警告,但不会因此建立可靠归属。修复应调整注册位置,而不是用延迟或吞掉 console 信息。
同步注册,异步结果只改变状态
下例同步登记 mounted 回调,同时在 Promise 续体里故意再次登记。挂载后只有 sync hook 会进入事件列表;late hook 没有归属实例,开发版会提示生命周期注入 API 只能在 setup 中使用。
实际业务若要等请求完成,可以先同步 onUnmounted 登记取消逻辑,或同步 onMounted 后在其内部启动任务。条件变化则由 ref、watch 或普通分支表达,不要等条件满足才临时补注册生命周期。
import { h, onMounted } from 'vue'
const hookEvents: string[] = []
const HookTimingProbe = {
setup() {
onMounted(() => hookEvents.push('sync hook'))
Promise.resolve().then(() => {
onMounted(() => hookEvents.push('late hook'))
})
return () => h('span', 'ready')
}
}挂载并等待微任务后,hookEvents 仍只有 sync hook。示例故意保留错误写法用于诊断;产品代码应删除迟到注册,把异步判断放进同步登记的钩子。
顶层 await 特例需要写清适用范围
<script setup> 中的顶层 await 会编译为 async setup,Vue 编译器会在 await 前后保存并恢复当前组件实例,因此它与手写 Promise.then、setTimeout 或普通 async 工具函数不同。使用该能力还要满足对应的 Suspense 与渲染方案。
面试回答最好先给默认规则,再说明这个受编译器支持的例外。不能把例外扩大到所有 await,也不能因为普通异步注册在某次测试中没有崩溃就认为它已生效;应观察钩子是否真正执行并保留开发警告。
容易答错的地方
- 把“同步注册”理解成钩子不能异步
- 钩子函数可以启动或等待异步工作,限制针对 onMounted 等 API 被调用的时刻。先同步登记,再在回调中处理异步逻辑,组件归属才稳定。
- 看到生产版不告警就继续使用
- 生产构建会减少诊断信息,但缺少当前实例的事实没有改变。测试应同时断言期望回调被调用,而不能以控制台安静作为正确证据。
面试官还会怎么问?
条件满足后才想执行 mounted 逻辑怎么办?
同步注册 onMounted,回调内读取条件;若条件可能在挂载后才变为真,再用同步创建的 watch 观察它,并保存清理逻辑。这样生命周期和条件各自表达自己的时序。
组合式函数中的生命周期钩子也要同步吗?
要。组合式函数如果在 setup 同步调用,其中的钩子会登记到调用组件;若组合式函数本身在异步回调中才调用,同样无法可靠取得组件实例。
script setup 顶层 await 后能注册 onMounted 吗?
编译后的顶层 await 会保留活动实例上下文,这是框架与编译器共同提供的特例。普通 async setup 的任意嵌套回调或外部 Promise 不能据此类推。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。