Appearance
✨ 组件生命周期 👌
要点速览
- 阶段:初始化 → 模板编译 → 挂载 → 更新 → 卸载;每个阶段有对应钩子。
- Vue3 钩子:
onBeforeMount/onMounted/onBeforeUpdate/onUpdated/onBeforeUnmount/onUnmounted;初始化阶段由setup()承担。 - 本质:在渲染器生命周期的关键时点执行你注册的回调(钩子)。
- 顺序(父子嵌套):父
onBeforeMount→ 子onBeforeMount→ 子onMounted→ 父onMounted(卸载同理递归)。 - 清理:在卸载阶段释放计时器、监听器、资源;在
KeepAlive场景使用onActivated/onDeactivated管理状态。
官方生命周期图:

阶段总览
这里分为这么几个大的阶段:
- 初始化选项式 API
- 模板编译
- 初始化渲染
- 更新组件
- 销毁组件
1. 初始化(选项式/组合式)
当渲染器遇到一个组件的时候,首先是初始化选项式 API,这里在内部还会涉及到组件实例的创建。
在组件实例创建的前后,对应如下钩子:
- 组件实例创建前:setup、beforeCreate
- 组件实例创建后:created
初始化阶段详解
- 选项解析与准备:读取选项对象,提取
props、data、render等配置,用于实例化与后续渲染。 - 创建组件实例:建立运行期实例对象(记录
state/isMounted/subTree等),并挂到vnode.component,形成与虚拟节点的绑定关系。 - 响应式状态初始化:
- 选项式:调用
data()后用响应式系统包裹,得到组件本地state;供渲染与更新依赖追踪。 - 组合式:执行
setup(props, context)创建ref/reactive/computed/watch等逻辑;<script setup>顶层声明在编译后自动暴露给模板。
- 选项式:调用
setup上下文:props(只读,含默认值与类型校验)、context(emit/slots/attrs/expose)。需要暴露实例方法时在此阶段调用expose或defineExpose。- 钩子触发顺序:先触发
beforeCreate(实例与状态尚未完全就绪),再准备好响应式state与实例后触发created(此时可安全访问state)。 - 渲染入口就绪:建立包裹渲染的副作用与调度器,为后续“挂载/更新”阶段提供执行入口;此时不涉及真实 DOM 操作。
2. 模板编译
接下来会进入模板编译的阶段,当模板编译的工作结束后,会执行 beforeMount 钩子函数。
模板编译阶段详解
- 触发时机:在 SFC 构建阶段(Vite/Vue 编译器)预编译;或在包含运行时编译器的构建中于运行期按需编译(学习/原型场景)。
- 编译流程:
- 解析:将模板解析为 AST(抽象语法树),识别元素、文本、插值、指令。
- 转换:将指令与结构化语法(
v-if/v-for/v-show/v-slot/v-model等)转换为渲染所需的中间节点,进行插槽归一化、绑定归一化。 - 代码生成:输出渲染函数
render(),使用虚拟节点工厂与 PatchFlags 标记可变边界,静态节点进行常量提升(hoist)。
- 产物与作用:
- 生成的
render()将在挂载与更新阶段被调用以产出 VNode 子树。 - PatchFlags 指示哪些属性/节点需要参与 diff,提高更新性能;静态提升减少重复创建。
- 插槽在编译期被整理为函数对象集合;子组件在运行期按需调用。
- 生成的
- 钩子衔接:模板编译完成后进入首次挂载流程,随后触发
onBeforeMount/beforeMount。
3. 初始化渲染(挂载)
接下来是初始化渲染,到了这个阶段,意味着已经生成了真实的 DOM. 完成初始化渲染后会执行 mounted 生命周期方法。
4. 更新组件
更新组件时对应着一组生命周期钩子方法:
- 更新前:beforeUpdate
- 更新后:updated
5. 卸载组件
销毁组件时也对应一组生命周期钩子方法:
- 销毁前:beforeUnmount
- 销毁后:unmounted
一般在销毁组件时我们会做一些清理工作,例如清除计时器等操作。
名称映射与并存(Vue2 vs Vue3)
在 Vue3 中生命周期钩子名称有所更新,但与 Vue2 的钩子可并存(不建议混用):
| 生命周期阶段 | Vue2 钩子 | Vue3 钩子 |
|---|---|---|
| 初始化(创建前后) | beforeCreate/created | setup |
| 挂载前/后 | beforeMount/mounted | onBeforeMount/onMounted |
| 更新前/后 | beforeUpdate/updated | onBeforeUpdate/onUpdated |
| 卸载前/后 | beforeDestroy/destroyed | onBeforeUnmount/onUnmounted |
Vue2 与 Vue3 钩子可并存,但 Vue3 钩子通常比同阶段的 Vue2 钩子更早执行。实际项目中不建议混用,以免增加心智负担。
概念与本质
生命周期就是在合适的时机调用你注册的回调函数。
首先需要了解组件实例和组件挂载。假设用户书写了这么一个组件:
jsx
// 选项式 API
export default {
name: "UserCard",
props: {
name: String,
email: String,
avatarUrl: String,
},
data() {
return {
foo: 1,
};
},
mounted() {
// ...
},
render() {
return h("div", { class: styles.userCard }, [
h("img", {
class: styles.avatar,
src: this.avatarUrl,
alt: "User avatar",
}),
h("div", { class: styles.userInfo }, [
h("h2", this.name),
h("p", this.email),
]),
]);
},
};这些内容是一个组件选项对象(并不是组件实例对象)。渲染该组件时,框架会创建并维护一个组件实例对象。组件实例对象本质上是一个记录组件运行期状态的对象,例如:
- 注册到组件的生命周期钩子函数
- 组件渲染的子树
- 组件是否已经被挂载
- 组件自身的状态
jsx
function mountComponent(vnode, container, anchor) {
// 获取【组件选项对象】
const componentOptions = vnode.type;
// 从【组件选项对象】上面提取出 render 以及 data
const { render, data } = componentOptions;
// 创建响应式数据
const state = reactive(data());
// 定义【组件实例对象】,一个组件实例本质上就是一个对象,它包含与组件有关的状态信息
const instance = {
// 组件自身的状态数据,即 data
state,
// 一个布尔值,用来表示组件是否已经被挂载,初始值为 false
isMounted: false,
// 组件所渲染的内容,即子树(subTree)
subTree: null,
};
// 将组件实例设置到 vnode 上,用于后续更新
vnode.component = instance;
// 后面逻辑略...
}运行机制(挂载与更新)
jsx
function mountComponent(vnode, container, anchor) {
// 前面逻辑略...
effect(
() => {
// 调用组件的渲染函数,获得子树
const subTree = render.call(state, state);
// 检查组件是否已经被挂载
if (!instance.isMounted) {
// 初次挂载,调用 patch 函数第一个参数传递 null
patch(null, subTree, container, anchor);
// 重点:将组件实例的 isMounted 设置为 true,这样当更新发生时就不会再次进行挂载操作,
// 而是会执行更新
instance.isMounted = true;
} else {
// 当 isMounted 为 true 时,说明组件已经被挂载,只需要完成自更新即可,
// 所以在调用 patch 函数时,第一个参数为组件上一次渲染的子树,
// 意思是,使用新的子树与上一次渲染的子树进行打补丁操作
patch(instance.subTree, subTree, container, anchor); // diff 计算
}
// 更新组件实例的子树
instance.subTree = subTree;
},
{ scheduler: queueJob }
);
}其核心是根据组件实例的 isMounted 属性判断是否初次挂载:
- 初次挂载:patch 的第一个参数为 null;会设置组件实例 isMounted 为 true
- 非初次挂载:更新组件的逻辑,patch 的第一个参数是组件上一次渲染的子树,从而和新的子树进行 diff 计算
所谓生命周期,就是在合适的时间执行用户传入的回调函数
jsx
function mountComponent(vnode, container, anchor) {
const componentOptions = vnode.type;
// 从【组件选项对象】中取得组件的生命周期函数
const {
render,
data,
beforeCreate,
created,
beforeMount,
mounted,
beforeUpdate,
updated,
} = componentOptions;
// 拿到生命周期钩子函数之后,就会在下面的流程中对应的位置调用这些钩子函数
// 在这里调用 beforeCreate 钩子
beforeCreate && beforeCreate();
// 创建响应式数据
const state = reactive(data());
// 创建组件实例对象
const instance = {
state,
isMounted: false,
subTree: null,
};
vnode.component = instance;
// 组件实例已经创建
// 此时在这里调用 created 钩子
created && created.call(state);
effect(
() => {
const subTree = render.call(state, state);
if (!instance.isMounted) {
// 初次挂载
// 在这里调用 beforeMount 钩子
beforeMount && beforeMount.call(state);
patch(null, subTree, container, anchor); // 创建 DOM
instance.isMounted = true;
// 在这里调用 mounted 钩子
mounted && mounted.call(state);
} else {
// 非初次挂载(更新)
// 在这里调用 beforeUpdate 钩子
beforeUpdate && beforeUpdate.call(state);
patch(instance.subTree, subTree, container, anchor);
// 在这里调用 updated 钩子
updated && updated.call(state);
}
instance.subTree = subTree;
},
{ scheduler: queueJob }
);
}在上面的代码中,首先从组件选项对象中获取到注册到组件上面的生命周期函数,然后内部会在合适的时机调用它们。
嵌套结构与调用顺序
组件之间是可以进行嵌套的,从而形成一个组件树结构。那么当遇到多组件嵌套的时候,各个组件的生命周期是如何运行的呢?
实际上非常简单,就是一个递归的过程。
假设 A 组件下面嵌套了 B 组件,那么渲染 A 的时候会执行 A 的 onBeforeMount,然后是 B 组件的 onBeforeMount,然后 B 正常挂载,执行 B 组件的 mounted,B 渲染完成后,接下来才是 A 的 mounted.
- 组件 A:onBeforeMount
- 组件 B:onBeforeMount
- 组件 B:mounted
- 组件 A:mounted
卸载递归
卸载流程与挂载相反,也遵循自顶向下递归:父 onBeforeUnmount → 子递归卸载 → 子 onUnmounted → 父 onUnmounted。
实际开发注意事项
生命周期钩子的执行顺序和时机
- 初始化:
setup()先于任何 Options API 钩子(包括beforeCreate/created)。 - 首次挂载:
onBeforeMount/beforeMount→ 首次渲染 →onMounted/mounted;父子顺序为父 beforeMount → 子 beforeMount → 子 mounted → 父 mounted。 - 更新:状态变化触发渲染副作用 →
onBeforeUpdate/beforeUpdate→ DOM diff →onUpdated/updated。 - 卸载:
onBeforeUnmount/beforeUnmount→ 清理副作用与资源 →onUnmounted/unmounted。 - 示例:在
onMounted中安全访问 DOM。jsconst el = ref(null); onMounted(() => el.value && el.value.focus());
- 初始化:
常见错误使用场景和最佳实践
- 错误:在
setup()或beforeMount访问真实 DOM。- 最佳实践:将 DOM 操作统一放在
onMounted/onUpdated,必要时配合nextTick()。
- 最佳实践:将 DOM 操作统一放在
- 错误:在
updated钩子中直接修改响应式状态,导致更新循环。- 示例(错误):js
onUpdated(() => { count.value++; }); - 最佳实践:使用
watch针对性响应或将副作用迁移到业务触发点。
- 示例(错误):
- 错误:直接解构
props失去响应性。- 最佳实践:使用
toRefs/toRef保持响应性。jsconst { options } = toRefs(props); // 正确
- 最佳实践:使用
- 错误:卸载时未清理资源。
- 最佳实践:在
onUnmounted清理计时器、事件、订阅与网络请求。jsonMounted(() => window.addEventListener("resize", onResize)); onUnmounted(() => window.removeEventListener("resize", onResize));
- 最佳实践:在
- 错误:在
性能优化相关注意事项
- 避免在
mounted阶段执行重计算或大量同步任务;必要时延迟到空闲时间。jsonMounted( () => requestIdleCallback?.(heavyWork) ?? setTimeout(heavyWork, 0) ); - 事件节流/防抖,减少频繁状态更新与重渲染。js
const onScroll = throttle(() => { /* update */ }, 200); onMounted(() => window.addEventListener("scroll", onScroll)); onUnmounted(() => window.removeEventListener("scroll", onScroll)); - 用
computed替代watch处理纯派生数据,减少冗余计算。 KeepAlive场景使用onActivated/onDeactivated管理数据刷新与订阅,避免每次进入都重新初始化。
- 避免在
与 Composition API 的结合使用要点
<script setup>顶层声明自动暴露到模板;宏必须顶层调用:defineProps/defineEmits/defineExpose/defineSlots/defineModel。- 仅在
onMounted/onUpdated做 DOM 相关副作用;在setup()中声明状态与纯逻辑。 - 父侧通过
ref获取子组件实例时,使用defineExpose显式暴露方法与数据。jsconst reset = () => { /* ... */ }; defineExpose({ reset });
