Vue 3 生命周期详解
原文标注的版本是 Vue 3.0.2,下面保留其“用示例和源码理解生命周期”的结构,并按 Vue 3.5.x 重新校正 API、调度器、错误处理、SSR、KeepAlive 和 Suspense。Vue 3.5.39 是本文示例的主要基线。
一、什么是生命周期?
组件从创建、挂载、更新到卸载,会在不同阶段调用不同的生命周期 hook。生命周期 hook 的作用不是让我们手动控制整个渲染过程,而是让组件在合适的阶段注册副作用、读取 DOM、清理资源或观察调试信息。
Vue 3 的生命周期可以分成以下几组:
| 场景 | Options API | Composition API |
|---|---|---|
| 创建 | beforeCreate、created | setup() 承担创建阶段逻辑 |
| 挂载 | beforeMount、mounted | onBeforeMount、onMounted |
| 更新 | beforeUpdate、updated | onBeforeUpdate、onUpdated |
| 卸载 | beforeUnmount、unmounted | onBeforeUnmount、onUnmounted |
| KeepAlive | activated、deactivated | onActivated、onDeactivated |
| 错误 | errorCaptured | onErrorCaptured |
| 调试 | renderTracked、renderTriggered | onRenderTracked、onRenderTriggered |
| SSR | serverPrefetch | onServerPrefetch |
Composition API 没有 onBeforeCreate 和 onCreated,通常把对应逻辑直接写在 setup() 或 <script setup> 的同步部分。

二、初始化阶段
Options API 的 beforeCreate 和 created
export default {
data() {
return {
value: 'hello',
}
},
beforeCreate() {
// data、computed、methods 等选项还没有完成实例代理
console.log('beforeCreate', this.value)
},
created() {
// data、computed、methods 已经可以访问
// 但 DOM 尚未挂载
console.log('created', this.value)
},
}
beforeCreate 适合了解实例创建顺序,但现代业务通常不需要在这里做很多工作。需要读写响应式状态、发起客户端请求时,可以在 created 或 Composition API 的 setup() 中处理;需要访问 DOM 则应等待 mounted。
Composition API 的 setup()
<script setup>
import { ref } from 'vue'
const value = ref('hello')
console.log(value.value)
</script>
在同时使用 Options API 和 Composition API 的组件中,setup() 会在 Options API 的 beforeCreate 之前执行。因此,“setup() 等价于 created”只是方便迁移的概念映射,并不是严格的一对一替换。
生命周期注册还必须同步进行:
<script setup>
import { onMounted } from 'vue'
// 正确:同步注册
onMounted(() => {})
// 不推荐:定时器执行时已经没有当前组件实例
setTimeout(() => {
// onMounted(() => {})
}, 100)
</script>
如果使用 async setup(),应在第一个 await 之前注册生命周期;<script setup> 对顶层 await 有额外的实例上下文恢复支持。
三、挂载阶段
beforeMount / onBeforeMount
这个 hook 在组件即将开始首次 DOM patch 前调用。此时组件的响应式状态已经建立,但最终 DOM 还没有完成挂载。不要在这里假设所有子组件或异步内容都已经存在。
export default {
beforeMount() {
console.log('beforeMount', this.$el)
},
}
Composition API 中,如果需要访问元素,应使用模板 ref,但在 onBeforeMount 中 ref 通常仍然是 null:
<script setup>
import { onBeforeMount, ref } from 'vue'
const root = ref(null)
onBeforeMount(() => {
console.log(root.value) // 通常仍未挂载
})
</script>
<template>
<div ref="root">Hello Vue</div>
</template>
mounted / onMounted
mounted 表示组件自己的 DOM 已创建并插入,且同步子组件通常也已经挂载。它不保证异步组件、Suspense 分支或所有后续异步数据已经完成。
<script setup>
import { onMounted, ref } from 'vue'
const root = ref(null)
onMounted(() => {
console.log(root.value)
})
</script>
<template>
<div ref="root">Hello Vue</div>
</template>
常见用途包括:
- 初始化需要 DOM 的第三方库;
- 添加
window/document事件监听器; - 读取元素尺寸或焦点;
- 启动只应在浏览器中运行的副作用。
如果需要在服务端获取数据,不要依赖 onMounted,应使用框架的数据获取能力或 onServerPrefetch()。
四、响应式更新阶段
renderTracked:依赖被 render effect 读取
renderTracked 是开发环境调试 hook,表示当前组件的 render effect 跟踪到了响应式依赖。它不是“页面上任意取值都会触发”,也不适合写业务逻辑。
Options API:
export default {
renderTracked(event) {
console.log('render tracked:', event)
},
}
Composition API:
<script setup>
import { onRenderTracked } from 'vue'
onRenderTracked(event => {
console.log('render tracked:', {
type: event.type,
key: event.key,
target: event.target,
})
})
</script>
常见 event.type 包括 get、has 和 iterate。这些调试 hook 只在开发构建中有意义,SSR 不执行。
renderTriggered:依赖触发 render effect
renderTriggered 表示已经被当前 render effect 依赖的响应式操作触发了重新渲染。
<script setup>
import { onRenderTriggered, ref } from 'vue'
const count = ref(0)
onRenderTriggered(event => {
console.log('render triggered:', {
type: event.type,
key: event.key,
oldValue: event.oldValue,
newValue: event.newValue,
})
})
</script>
<template>
<p>{{ count }}</p>
<button @click="count++">加 1</button>
</template>
常见触发类型包括 set、add、delete 和 clear。如果某个响应式值没有被当前组件模板读取,修改它不会触发该组件的 renderTriggered。
beforeUpdate / onBeforeUpdate
在组件因响应式状态变化而重新渲染时,beforeUpdate 在本次 DOM patch 前调用:
export default {
data() {
return {
value: 0,
}
},
beforeUpdate() {
console.log('DOM update is about to start:', this.value)
},
}
这里可以读取更新前的 DOM 状态,但不建议在其中无条件修改响应式状态,否则可能造成额外更新或循环。
updated / onUpdated
updated 在本次 DOM patch 完成后调用。Vue 会批量合并同一轮同步修改,因此多次同步赋值不一定触发多次 updated。
<script setup>
import { nextTick, onUpdated, ref } from 'vue'
const value = ref(0)
onUpdated(() => {
console.log('DOM updated')
})
async function update() {
value.value++
await nextTick()
// 等待本轮 DOM 更新完成
}
</script>
不要在 updated 中无条件修改组件依赖的状态;如果只是需要等待某次 DOM 更新,优先在修改状态的函数中使用 nextTick()。
五、卸载阶段
beforeUnmount / onBeforeUnmount
组件开始卸载前调用,此时实例仍然可用,适合清理事件监听器、定时器、WebSocket、观察器和第三方实例:
<script setup>
import { onMounted, onBeforeUnmount } from 'vue'
function handleResize() {
console.log(window.innerWidth)
}
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize)
})
</script>
unmounted / onUnmounted
组件卸载完成后调用:
<script setup>
import { onUnmounted } from 'vue'
onUnmounted(() => {
console.log('component unmounted')
})
</script>
手动注册的副作用应在 onBeforeUnmount 或 onUnmounted 清理。普通 ref、computed 和组件内部 watcher 通常会随组件作用域一起停止,但异步回调中创建的 watcher 可能不再自动绑定到组件,需要手动停止。
六、错误捕获
errorCaptured 的传播方向
errorCaptured 捕获的是后代组件产生的错误,错误会沿父链由近及远传播,类似事件冒泡:
错误来源组件
-> 最近的父组件 errorCaptured
-> 更外层祖先 errorCaptured
-> app.config.errorHandler
错误来源组件自身的 errorCaptured 不会捕获自己的错误。捕获 hook 返回 false 后,会阻止错误继续传递给后续祖先和全局 handler;默认情况下,即使某个 hook 已记录错误,仍可能继续交给 app.config.errorHandler。
可运行的 Composition API 示例
import { createApp, defineComponent } from 'vue'
const Child = defineComponent({
setup() {
throw new Error('child setup failed')
},
template: '<div>Child</div>',
})
const Parent = defineComponent({
components: { Child },
errorCaptured(error, instance, info) {
console.error('parent captured:', error, info)
return false
},
template: '<Child />',
})
const app = createApp({
components: { Parent },
template: '<Parent />',
})
app.config.errorHandler = (error, instance, info) => {
console.error('global handler:', error, info)
}
app.mount('#app')
由于 Parent.errorCaptured() 返回了 false,child 的错误不会继续传给全局 handler。这个结果依赖错误来源、祖先结构和返回值,不应把某一组控制台输出当成所有场景的固定顺序。
常见错误来源
Vue 的错误处理覆盖 setup()、render、生命周期、watcher、事件处理器、指令和 transition 等。errorCaptured 适合记录和展示降级 UI,不应把所有错误都静默吞掉。
七、KeepAlive 生命周期
<KeepAlive> 会缓存动态组件实例。切换缓存组件时,组件一般不会被卸载,而是触发:
activated/onActivated:组件被激活;首次挂载时也会调用。deactivated/onDeactivated:组件被移出活动树;最终卸载时也会调用。
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
console.log('tab activated')
})
onDeactivated(() => {
console.log('tab deactivated')
})
</script>
<template>
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>
</template>
如果只是切换 KeepAlive 的 tab,不应期待每次都重新执行 mounted/unmounted。需要保存滚动位置、暂停轮询或恢复第三方资源时,可以使用 activated/deactivated。
八、SSR、Suspense 与异步内容
SSR
SSR 中通常会执行 setup() 和创建阶段逻辑,但不会执行依赖浏览器 DOM 的 hook:
会执行:setup、created 及可能的 serverPrefetch
不会执行:mounted、updated、unmounted、activated 等 DOM/客户端生命周期
使用 onServerPrefetch():
<script setup>
import { onServerPrefetch, ref } from 'vue'
const user = ref(null)
onServerPrefetch(async () => {
user.value = await fetchUser()
})
</script>
SSR renderer 会等待 onServerPrefetch() 返回的 Promise。普通 onMounted(async () => {}) 不会在服务端执行,也不会让 SSR 等待。
Suspense
Suspense 可以等待异步 setup 和异步组件:
<template>
<Suspense>
<AsyncPanel />
<template #fallback>
正在加载……
</template>
</Suspense>
</template>
Suspense pending 时,异步分支的 post-render effects 可能先缓存在 boundary 中。因此父组件 mounted 不表示 Suspense 内的异步后代已经 mounted。Suspense 当前仍属于需要关注版本和边界的高级能力。
九、源码与调度器简析
原文的源码片段来自 Vue 3.0.2,以下结论仍然有效,但字段和队列已更新:
apiLifecycle.ts的createHook/injectHook把 callback 包装后保存到组件实例的 hook 数组。- renderer 在组件首次 render effect 中同步调用 beforeMount,在 patch 后排队 mounted。
- 更新时同步调用 beforeUpdate,在 patch 后排队 updated。
scheduler.ts用主队列和 post 队列批量处理任务;Vue 3.5 用QUEUED、PRE、ALLOW_RECURSE、DISPOSEDflags 管理去重和失效。- watcher 默认
flush: 'pre',post watcher 走 post-render effect,sync watcher 直接运行。 nextTick()等待当前 flush Promise,而不是等待所有任意异步请求。
Vue 3.5 不再使用原文中独立的 pendingPreFlushCbs 作为默认 pre watcher 主模型,也不能把 queuePreFlushCb 当成当前公共 API。精确实现请阅读:
十、总结
setup()承担 Composition API 的创建阶段逻辑。onMounted、onUpdated等 hook 必须在当前组件实例存在时同步注册。- before 类 hook 通常在对应渲染/卸载流程中同步执行,mounted/updated/unmounted 通常通过 post-render 调度。
renderTracked/renderTriggered只用于开发环境调试 render effect。- 错误从来源组件沿父链向上捕获,
return false才会停止后续传播。 - KeepAlive 使用 activated/deactivated,Suspense 和异步组件会改变普通父子生命周期的简单顺序。
- SSR 使用
onServerPrefetch获取服务端数据,普通 DOM 生命周期不在 SSR 调用。
官方参考
原文出处
作者:摆烂学前端
原文链接:https://juejin.cn/post/7020017329815683085
来源:稀土掘金。著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。