在上一篇文章中,我们深入探讨了组件从VNode到DOM的渲染过程。本篇文章将聚焦于组件的生命周期------这个贯穿组件从创建到销毁整个过程的时间轴。理解生命周期,不仅能帮助我们写出更可靠的代码,还能在合适的时机做合适的事情。
前言:为什么需要生命周期?
想象一下,我们正在搭建一座房子,一般需要经过以下几个阶段:
- 创建阶段:设计图纸、准备材料
- 挂载阶段:打地基、砌墙、安装门窗
- 更新阶段:翻新墙面、更换家具
- 卸载阶段:拆除房屋、清理场地
组件也是如此。在它的整个生命周期中,我们需要在不同的时间点执行不同的操作:
javascript
const Component = {
// 创建时:初始化数据
created() {
this.fetchData();
},
// 挂载后:操作DOM
mounted() {
this.$el.focus();
},
// 更新时:页面刷新
updated() {
this.$el.scrollTop = this.$el.scrollHeight;
}
// 卸载前:清理资源
beforeUnmount() {
clearInterval(this.timer);
}
};
生命周期的完整图谱
生命周期全景图
下面这张图展示了 Vue3 组件的完整生命周期流程: 
各个阶段的核心任务
| 阶段 | 钩子 | 核心任务 | 注意事项 |
|---|---|---|---|
| 创建阶段 | setup / beforeCreate / created | 初始化数据、设置响应式 | 无法访问DOM |
| 挂载阶段 | beforeMount / mounted | 渲染DOM、操作DOM | 可以访问DOM |
| 更新阶段 | beforeUpdate / updated | 响应数据变化 | 避免在更新钩子中修改数据 |
| 卸载阶段 | beforeUnmount / unmounted | 清理资源 | 清除定时器、取消订阅 |
| 缓存阶段 | activated / deactivated | 配合keep-alive | 缓存组件的激活/失活 |
创建阶段:组件诞生
创建阶段的三个钩子
在 Vue3 中,创建阶段实际上由三个关键步骤组成:
- 最先执行:setup
- 然后执行:beforeCreate
- 最后执行:created
javascript
export default {
// 1. 最先执行:setup
setup() {
console.log('1. setup 执行');
const count = ref(0);
// setup 中不能使用 this
// console.log(this); // undefined
return { count };
},
// 2. 然后执行:beforeCreate
beforeCreate() {
console.log('2. beforeCreate 执行');
console.log('数据尚未初始化:', this.count); // undefined
console.log('DOM 尚未创建:', this.$el); // undefined
},
// 3. 最后执行:created
created() {
console.log('3. created 执行');
console.log('数据已初始化:', this.count); // 0
console.log('DOM 尚未创建:', this.$el); // undefined
}
};
各钩子的数据访问能力
为了更好地理解每个阶段能做什么,我们用一个表格来展示:
| 钩子 | 访问 data | 访问 props | 访问 computed | 访问 methods | 访问 DOM | 访问 $el |
|---|---|---|---|---|---|---|
| setup | ✅ | ✅ | ❌ (尚未创建) | ❌ (尚未创建) | ❌ | ❌ |
| beforeCreate | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| created | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
为什么需要三个创建钩子?
你可能有这样的疑问:为什么有了 setup 还要保留 beforeCreate 和 created?
这其实是为了兼容性和渐进迁移。在 Vue3 中,setup 实际上是 beforeCreate 和 created 的替代品;但是 Vue3 为了向下兼容 Vue2 ,仍然保留了 beforeCreate 和 created :
Vue2 风格的创建钩子:
javascript
export default {
beforeCreate() {
// 初始化非响应式数据
this.nonReactive = {};
},
created() {
// 发起API请求
this.fetchData();
}
};
Vue3 组合式风格:
javascript
export default {
setup() {
// 初始化非响应式数据
const nonReactive = {};
// 发起API请求
fetchData();
return { nonReactive };
}
};
挂载阶段:组件展现
挂载过程的内部机制
挂载阶段是组件第一次将虚拟 DOM 渲染为真实 DOM 的过程:
javascript
export default {
beforeMount() {
console.log('1. beforeMount 执行');
console.log('此时已有编译好的模板,但尚未挂载到DOM');
console.log('DOM 尚不存在:', this.$el); // undefined
},
// render 函数在 beforeMount 之后、mounted 之前执行
render() {
console.log('2. render 执行');
return h('div', 'Hello World');
},
mounted() {
console.log('3. mounted 执行');
console.log('DOM 已创建并挂载:', this.$el); // <div>Hello World</div>
console.log('可以安全地操作DOM了');
// 可以访问DOM元素
this.$el.querySelector('input')?.focus();
// 可以集成第三方库
new Chart(this.$el.querySelector('#chart'), {...});
}
};
挂载阶段的时序图

挂载阶段的典型应用场景
1. 操作DOM
javascript
this.$el.scrollTop = 100;
2. 获取元素尺寸
javascript
const width = this.$refs.box.offsetWidth;
3. 集成第三方库(需要DOM存在)
javascript
this.chart = new Chart(this.$refs.canvas, {
type: 'line',
data: this.chartData
});
4. 添加全局事件监听
javascript
window.addEventListener('resize', this.handleResize);
5. 启动定时器
javascript
this.timer = setInterval(this.refreshData, 5000);
6. 卸载前清理操作
javascript
window.removeEventListener('resize', this.handleResize);
clearInterval(this.timer);
this.chart?.destroy();
更新阶段:组件响应
更新阶段的时序图

更新阶段的注意事项
javascript
export default {
beforeUpdate() {
// ✅ 可以在DOM更新前访问旧状态
const oldHeight = this.$refs.box.offsetHeight;
console.log('旧高度:', oldHeight);
// ❌ 不要在更新钩子中修改数据(可能造成死循环)
// this.count++; // 会触发无限循环
// ✅ 可以在这里手动操作DOM(不推荐)
// 但要注意这些操作可能会被后续的更新覆盖
},
updated() {
// ✅ 可以获取更新后的DOM信息
const newHeight = this.$refs.box.offsetHeight;
console.log('新高度:', newHeight);
// ✅ 可以根据新状态调整其他非响应式内容
if (newHeight > 500) {
this.$refs.box.classList.add('overflow');
}
// ❌ 同样避免在这里修改数据
// ❌ 避免直接操作DOM来"修复"样式,应该通过数据驱动
}
};
卸载阶段:组件消亡
卸载的完整过程
javascript
export default {
beforeUnmount() {
console.log('1. beforeUnmount 执行');
console.log('组件即将被卸载,但依然可以访问');
console.log('DOM 仍然存在:', this.$el);
// 清理工作
this.cleanup();
},
unmounted() {
console.log('2. unmounted 执行');
console.log('组件已被卸载');
console.log('DOM 已移除:', this.$el); // 被移除或置空
// 最终清理
this.finalCleanup();
}
};
需要清理的典型资源
- 定时器:
clearInterval(timer); - 事件监听:
window.removeEventListener('resize', handleResize); - 观察者:
observer.disconnect(); - 网络请求:
controller.abort(); - 第三方库实例:
chart.destroy(); - 手动订阅:
subscription.unsubscribe();
缓存阶段:KeepAlive 的特殊生命周期
为什么需要缓存阶段?
当组件被 <KeepAlive> 包裹时,它的生命周期会发生变化: 
activated 和 deactivated 的使用
javascript
const CacheComponent = {
setup() {
console.log('setup 执行'); // 只执行一次
const count = ref(0);
// 这些钩子会在组件被缓存时特殊处理
onMounted(() => {
console.log('mounted 执行'); // 只执行一次
});
onActivated(() => {
console.log('activated 执行'); // 每次进入视图时执行
// 恢复一些状态
startAnimation();
startPolling();
});
onDeactivated(() => {
console.log('deactivated 执行'); // 每次离开视图时执行
// 暂停一些操作,但不销毁
stopAnimation();
stopPolling();
});
onUnmounted(() => {
console.log('unmounted 执行'); // 最终销毁时执行
});
return { count };
}
};
父子组件的生命周期顺序
挂载阶段的执行顺序
当父子组件嵌套时,生命周期的执行顺序非常关键:
javascript
const Parent = {
setup() { console.log('Parent setup'); },
beforeCreate() { console.log('Parent beforeCreate'); },
created() { console.log('Parent created'); },
beforeMount() { console.log('Parent beforeMount'); },
mounted() { console.log('Parent mounted'); },
render() {
return h('div', [
h(Child)
]);
}
};
const Child = {
setup() { console.log('Child setup'); },
beforeCreate() { console.log('Child beforeCreate'); },
created() { console.log('Child created'); },
beforeMount() { console.log('Child beforeMount'); },
mounted() { console.log('Child mounted'); }
};
// 渲染输出顺序:
// 1. Parent setup
// 2. Parent beforeCreate
// 3. Parent created
// 4. Parent beforeMount
// 5. Child setup
// 6. Child beforeCreate
// 7. Child created
// 8. Child beforeMount
// 9. Child mounted
// 10. Parent mounted
更新阶段的执行顺序
当 Parent 的数据变化时:
- Parent beforeUpdate
- Child beforeUpdate
- Child updated
- Parent updated
卸载阶段的执行顺序
当父组件被移除时:
- Parent beforeUnmount
- Child beforeUnmount
- Child unmounted
- Parent unmounted
执行顺序的规律总结
| 阶段 | 执行顺序 | 原因 |
|---|---|---|
| 创建 | 父 → 子 | 父组件先创建,才能传递props给子组件 |
| 挂载 | 子 → 父 | 父组件需要等待所有子组件挂载完成 |
| 更新 | 父 → 子 | 父组件数据变化,传递给子组件 |
| 卸载 | 子 → 父 | 先拆除内部,再拆除外部 |
Vue3 中两种写法的生命周期对比
Vue3 同时支持两种写法:选项式 API 与 组合式 API。
选项式 API 生命周期
- beforeCreate / created
- beforeMount / mounted
- beforeUpdate / updated
- beforeUnmount / unmounted
- activated / deactivated
- errorCaptured
- renderTracked / renderTriggered:新增的调试钩子
组合式 API 生命周期
- setup
- onBeforeMount / onMounted
- onBeforeUpdate / onUpdated
- onBeforeUnmount / onUnmounted
- onActivated / onDeactivated
- onErrorCaptured
- onRenderTracked / onRenderTriggered
两种写法的对应关系表
| 选项式 API | 组合式 API | 执行时机 |
|---|---|---|
| beforeCreate/created | 直接在 setup 中编写代码 | 组件初始化前/组件初始化后 |
| beforeMount/mounted | onBeforeMount/onMounted | DOM 挂载前/DOM 挂载后 |
| beforeUpdate/updated | onBeforeUpdate/onUpdated | 数据更新、DOM 更新前/DOM 更新后 |
| beforeUnmount/unmounted | onBeforeUnmount/onUnmounted | 组件卸载前/组件卸载后 |
| activated/deactivated | onActivated/onDeactivated | keep-alive 组件激活/keep-alive 组件失活 |
| errorCaptured | onErrorCaptured | 捕获后代组件错误 |
核心差异:setup 中的生命周期
setup 函数是最早的生命周期钩子,本身执行在 beforeCreate 和 created 之前,属于 beforeCreate 和 created 的替代品,因此在 setup 中编写的代码相当于在这两个钩子中执行:
javascript
export default {
setup() {
// 这些代码相当于在 beforeCreate 和 created 中执行
console.log('相当于 beforeCreate/created');
const count = ref(0);
// 可以在这里执行初始化操作
fetchData();
// 注册生命周期钩子
onMounted(() => {
console.log('mounted');
});
return { count };
}
};
<script setup> 的特殊性
<script setup> 的本质
<script setup> 是组合式 API 的语法糖,它在编译时会被转换为普通的 setup() 函数:
html
<!-- 源码写法 -->
<script setup>
import { ref, onMounted } from 'vue';
const count = ref(0);
function increment() {
count.value++;
}
onMounted(() => {
console.log('组件已挂载');
});
</script>
javascript
// 编译后
export default {
setup() {
const count = ref(0);
function increment() {
count.value++;
}
onMounted(() => {
console.log('组件已挂载');
});
return { count, increment };
}
};
<script setup> 中的生命周期变化
在 <script setup> 中,生命周期钩子的使用变得更加简洁:
- onBeforeMount / onMounted
- onBeforeUpdate / onUpdated
- onBeforeUnmount / onUnmounted
- onActivated / onDeactivated
<script setup> 的特殊特性
javascript
<script setup>
// 1. 自动返回顶层变量
const count = ref(0); // 自动暴露给模板
function increment() {} // 自动暴露给模板
// 2. 支持顶层 await
const data = await fetchData(); // 组件会等待异步操作完成
// 3. 使用 defineProps 和 defineEmits
const props = defineProps({
title: String
});
const emit = defineEmits(['update']);
// 4. 使用 defineExpose 暴露方法
defineExpose({
resetCount: () => count.value = 0
});
// 5. 生命周期钩子可以直接使用
onMounted(() => {
console.log('mounted');
});
</script>
生命周期的最佳实践
各阶段适合做什么
| 阶段 | 适合的操作 | 不适合的操作 |
|---|---|---|
| setup/created | 初始化数据、设置响应式、发起API请求 | 操作DOM、访问$el |
| beforeMount | 最后一次数据修改机会 | 操作DOM |
| mounted | 操作DOM、集成第三方库、添加事件监听 | 同步修改数据(可能触发额外更新) |
| beforeUpdate | 访问更新前的DOM | 修改数据(可能死循环) |
| updated | 执行依赖更新后DOM的操作 | 修改数据(可能死循环) |
| beforeUnmount | 清理资源、移除事件监听 | 异步操作 |
| unmounted | 最终清理 | 访问已销毁的实例 |
生命周期调试技巧
使用生命周期追踪
javascript
<script setup>
import { onMounted, onUpdated, onRenderTracked, onRenderTriggered } from 'vue';
// 追踪渲染依赖
onRenderTracked((event) => {
console.log('渲染依赖追踪:', event);
// {
// key: 'count', // 依赖的属性名
// target: {}, // 依赖的目标对象
// type: 'get', // 操作类型
// }
});
// 追踪渲染触发原因
onRenderTriggered((event) => {
console.log('渲染触发原因:', event);
// {
// key: 'count',
// target: {},
// type: 'set',
// oldValue: 0,
// newValue: 1
// }
});
// 记录完整生命周期
onBeforeMount(() => console.log('🔄 beforeMount'));
onMounted(() => console.log('✅ mounted'));
onBeforeUpdate(() => console.log('🔄 beforeUpdate'));
onUpdated(() => console.log('✅ updated'));
onBeforeUnmount(() => console.log('🔄 beforeUnmount'));
onUnmounted(() => console.log('✅ unmounted'));
</script>
使用钩子组合
javascript
// 创建可复用的生命周期逻辑
function useLogger(componentName) {
onBeforeMount(() => {
console.log(`${componentName} 准备挂载`);
});
onMounted(() => {
console.log(`${componentName} 已挂载`);
});
onBeforeUnmount(() => {
console.log(`${componentName} 准备卸载`);
});
onUnmounted(() => {
console.log(`${componentName} 已卸载`);
});
}
// 在组件中使用
<script setup>
const props = defineProps({ name: String });
useLogger(props.name);
</script>
结语
理解生命周期,就像是掌握了组件从生到死的完整剧本。知道在每个阶段该做什么、不该做什么,才能写出既高效又可靠的Vue应用。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!