Vue的生命周期阶段
Vue 组件的生命周期指的是一个组件从创建、挂载、更新到销毁的整个过程。
Vue 在这个过程中的不同阶段会自动调用特定的函数,这些函数被称为生命周期钩子(Lifecycle Hooks),你可以在这些钩子中编写代码来执行特定操作。
Vue 组件的生命周期主要分为四个大阶段,每个阶段包含若干具体的钩子函数
避免使用箭头函数:定义生命周期钩子时,不要使用箭头函数,因为这会导致无法通过 this 获取组件实例。
创建阶段 (Creation)
这个阶段发生在组件实例被创建之后、挂载到 DOM 之前。
beforeCreate: 实例刚被创建,数据观测和事件配置之前被调用。此时无法访问data、methods等。created: 实例已创建完成,数据观测、属性和方法的运算、事件回调已配置。可以访问data和methods,但 DOM 还未生成,$el属性尚不存在。
挂载阶段 (Mounting)
这个阶段发生在组件被挂载到 DOM 树上时。
beforeMount: 在挂载开始之前被调用,相关的渲染函数首次被调用。此时模板已编译,但尚未挂载到页面。mounted: 实例被挂载到 DOM 后调用。此时可以访问到 DOM 元素,常用于执行需要操作 DOM 的初始化代码(如初始化第三方库)。
更新阶段 (Updating)
当组件的响应式数据发生变化时触发。
beforeUpdate: 数据更新时调用,发生在虚拟 DOM 打补丁之前。适合在更新之前访问现有的 DOM。updated: 数据更改导致虚拟 DOM 重新渲染和打补丁后调用。此时 DOM 已经更新。
销毁阶段 (Destruction)
当组件实例被销毁时触发。
beforeDestroy: 实例销毁之前调用。此时实例仍然完全可用,常用于清除定时器、解绑事件监听器等。destroyed: 实例销毁后调用。所有指令被解绑,事件监听器被移除,子实例也被销毁。
生命周期钩子函数
创建周期的钩子函数
Vue 组件的创建阶段是生命周期的第一个阶段,发生在组件实例化之后、挂载到 DOM 之前。这个阶段的核心是初始化组件的内部状态和逻辑。
beforeCreate
Vue3写到 setup() 中
beforeCreate
是组件生命周期中第一个被调用的钩子函数。
- 触发时机:组件实例刚被创建,但是在数据观测 (data observer) 和事件 / 生命周期钩子的初始化之前。
- 可访问性:此时无法访问
data、methods、computed、watch等实例属性和方法。this上下文虽然存在,但指向的实例还未完全初始化。- 在这个阶段,数据是获取不到的,并且真实dom元素也是没有渲染出来的
- 典型用途:几乎很少使用,因为此时实例还未准备好。偶尔用于添加一些非常早期的全局事件监听。
这个例子可以对比 beforeCreate 和 created
的访问能力
1 | <template> |
created
Vue3写到 setup() 中
created
是创建阶段的第二个,也是最重要的钩子函数之一。
- 触发时机:组件实例已经完成了数据观测、属性和方法的运算、watch/event 事件回调的配置之后。
- 可访问性:此时可以访问
data、methods、computed、watch等,但 DOM 还未生成,挂载阶段还没开始,$el属性尚不存在。- 也就是说,在这个阶段,可以访问到数据了,但是页面当中真实dom节点还是没有渲染出来,在这个钩子函数里面,可以进行相关初始化事件的绑定、发送请求操作
- 典型用途:这是进行数据初始化、调用异步 API 获取数据、设置监听事件的最佳时机之一。
例如,我们可以这样在 created 中设置定时器和事件监听
1 | <template> |
挂载阶段的钩子函数
阶段预览
在 created 钩子执行之后,挂载阶段随即开始。Vue 内部会执行以下逻辑:
- 编译模板(Compiler): 检查是否有 template 选项或 el 选项(对于根实例)。Vue 将模板编译成 渲染函数(render function)。
- 触发 beforeMount: 在渲染开始之前调用。
- 执行渲染与打补丁(Render & Patch):
- 调用 render 函数生成 虚拟 DOM (VNode)。
- Vue 根据虚拟 DOM 生成真实的 DOM 节点。
- 将真实的 DOM 节点替换或插入到页面中对应的挂载点(el)。
- 触发 mounted: 真实 DOM 渲染完毕后调用。
beforeMount
Vue3对应onBeforeMount()
在 beforeMount 中,组件已经完成了响应式状态的设置,data, methods, computed 都已可用,而且此时,HTML 模板已经编译好了,但尚未渲染到浏览器页面上。
但是此时
- 无法获取到真实的 DOM 节点。
this.$el(Vue 2) 或模板中的ref此时都还不存在或不可用。
这个生命周期钩子函数非常少用。大多数数据初始化的逻辑通常在 created 中进行。在极少数情况下,你可能需要在渲染前最后一次修改数据,且不希望触发额外的重新渲染流程,才会在此时操作。
1 | <template> |
mounted
Vue3 对应 onMounted()
mounted就是挂载完成后的阶段,此时组件的视图已经成功渲染到了浏览器的页面上。原来的挂载点(如
<div id="app"></div>)已经被 Vue 组件生成的 DOM
替换。
对于此时的 DOM 状态,如下
- 可以访问真实的 DOM 元素。
- 可以使用 document.querySelector,或者通过 this.$refs 访问模板引用。
这个生命周期的钩子函数非常重要
- 操作 DOM: 如果你需要直接修改 DOM(虽然不推荐,但在集成第三方库时很有用)。
- 初始化第三方库: 比如初始化 ECharts 图表、Swiper 轮播图、D3.js 可视化、富文本编辑器等。因为这些库通常需要一个真实存在的 DOM 节点作为容器。
- 设置焦点: 例如页面加载后自动聚焦到输入框 (input.focus())。
但是,此时不会承诺所有子组件也都一起挂载完成:
虽然通常情况下子组件先挂载,但如果你使用了异步组件(Async Components)或
<Suspense>,mounted
不会等待它们。如果你需要确保整个视图(包括所有子组件)都渲染完毕,应使用
this.$nextTick。
例如,在 mounted 中,我们通常初始化第三方库
1 | <template> |
mounted钩子:这是初始化的关键时机。此时组件已经挂载到 DOM 树上,document.getElementById('visitorChart')可以获取到真实的 DOM 元素,第三方库才能正确找到渲染目标。- 存储实例引用:将创建的图表实例存储在
data中(chartInstance),便于后续操作(如更新数据、销毁实例)。 - 清理工作:在
beforeDestroy钩子中调用图表实例的destroy()方法,释放资源,避免内存泄漏。
父子组件的挂载顺序
当父组件包含子组件时,挂载阶段的执行顺序是 “由外向内创建,由内向外挂载”。
假设结构是:Parent -> Child
- Parent beforeCreate
- Parent created
- Parent beforeMount (父组件准备挂载,解析模板发现有子组件)
- Child beforeCreate
- Child created
- Child beforeMount
- Child mounted (子组件先挂载完成)
- Parent mounted (父组件最后挂载完成)
结论: 在父组件的 mounted 中,你可以放心地通过
$refs 访问子组件,因为此时子组件一定已经挂载好了。
更新阶段的钩子函数
阶段预览
更新阶段(Update Phase) 是 Vue 生命周期中最“繁忙”的阶段。与挂载阶段只执行一次不同,更新阶段会随着用户操作、接口请求导致的数据变化而反复执行。
简单来说,只要页面上用的响应式数据变了,Vue 就会执行:数据变了 -> 重新计算 -> 重新渲染 DOM。
当组件的 响应式数据(data/props/computed) 发生变更,且这些数据正在被模版(Template)使用时,更新流程就会触发:
- 数据变更: 你执行了
this.message = 'New Value'。 - 触发 beforeUpdate: 数据已经变了,但 DOM 还没变。
- Virtual DOM 重新渲染与 Patch: Vue 内部计算新旧虚拟 DOM 的差异(Diff 算法)。
- 真实 DOM 更新: 将差异应用到浏览器页面上。
- 触发 updated: 此时页面已经显示了最新的数据。
beforeUpdate
这是更新前的钩子函数,此时数据已经更新了,但 DOM 还在“排队”等待更新。
那么此时页面上的状态有这样的特质:数据是新的,渲染的视图和页面还是旧的(显示修改前的数据或者状态)
那么这个时候就非常适合在 DOM 更新前访问现有的 DOM。比如:获取更新前的滚动条位置,或者手动移除已添加的事件监听器。也就是在现有 DOM 将要被更新之前访问它
Vue 3 对应: onBeforeUpdate
1 | <template> |
- 你会发现 onBeforeUpdate 就像是一个快照,它保留了页面变身前的最后一刻。
当父组件数据变化,通过 props 传给子组件时,更新顺序是怎样的?
- Parent
beforeUpdate - Child
beforeUpdate - Child
updated(子组件先更新完 DOM) - Parent
updated(父组件最后更新完)
“父先准备,子先完成,父最后完成”
updated
Vue 3 对应: onUpdated
这就是更新后的钩子函数了,执行它的时候,DOM 已经根据最新的数据重新渲染完毕。此时页面上的数据是新的,视图也是新的
这个函数非常常用,例如当数据变化导致视图变化后,需要执行依赖于新 DOM 的操作。
在类似微信的聊天界面中,当发送新消息(列表数据更新)后,滚动条应该自动跳到最底部,让用户看到最新消息。
1 | <template> |
那么这里为什么不用 watch,如果你用 watch
监听 messages,代码执行时数据变了,但 DOM
还没渲染新消息。此时获取的 scrollHeight
还是旧的高度,滚动条会滚到倒数第二条消息的位置。只有在
updated 中,DOM 才是包含最新消息的状态。
updated vs this.$nextTick (或
nextTick)
这两个确实很像,但是通常情况下不能混用
- updated:是一个全局的钩子。只要该组件内任何响应式数据变化导致视图更新,它都会触发。如果你的组件很复杂,里面有几十个变量,其中一个变了,updated 就会跑一次,这可能导致性能浪费或逻辑混乱。
- nextTick:通常结合具体逻辑使用。例如我只想在这次修改数据后,等待 DOM 更新做某事。
如果你只想针对特定操作(比如点击发送按钮)后的 DOM 更新做处理,请使用 nextTick,而不是在 updated 里写大段的 if…else 判断。
所以我上面的例子只是一个例子,事实上没有人会这样写
注意,千万不要尝试在 updated 钩子中去修改数据,这是非常危险的操作。
1 | <script setup> |
- 你修改了 count。
- 触发 updated。
- 在 updated 里,你又执行了 count.value++。
- 数据变了 -> 再次触发更新 -> 再次触发 updated -> 再次 count++…
- 结果: 浏览器卡死(Infinite Loop),或者 Vue 报错提示检测到无限递归。
销毁阶段的钩子函数
阶段预览
这个阶段的函数都在实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。
在 Vue 2 中,这个阶段被称为 Destroy;在 Vue 3 中,为了语义更准确,改名为 Unmount(卸载)。虽然名字变了,但核心逻辑是一样的:清理资源,防止内存泄漏。
什么时候会触发销毁?
组件并不是随时都会被销毁的。以下情况会触发销毁流程:
- 条件渲染: 使用
v-if="false"(注意:v-show只是隐藏,不会销毁)。 - 路由切换: 切换页面(Vue Router)时,老页面的组件通常会被销毁。
- 父组件销毁: 覆巢之下无完卵,父组件被移除,子组件也会随之销毁。
- 手动调用: 调用
app.unmount()(Vue 3)或vm.$destroy()(Vue 2,极少用)。
beforeDestroy
Vue3对应:onBeforeUnmount
这个函数执行的时候,组件即将被卸载,但是它还存在,而且组件实例依然完全可用。数据(Data)、方法(Methods)、DOM 节点都还在。
在这里最核心的就是进行清理工作:
- 清除定时器 (
setInterval,setTimeout等)。 - 解绑全局事件 (
window.removeEventListener)。 - 销毁第三方实例 (ECharts, Mapbox 等)。
- 取消未完成的网络请求。
如果你不写这些清理代码,你的应用在运行一段时间后,内存占用会越来越高,网页会越来越卡,这叫内存泄漏 (Memory Leak)。
例如,我们需要在beforeDestroy
这个阶段进行清除定时器,这是一个倒计时组件。如果你离开这个页面时没有清除定时器,它是不会自动停止的!它会在后台一直跑,报错而且消耗
CPU。
1 | <template> |
如果注释掉 onBeforeUnmount 里的代码,当你切换路由去别的页面时,控制台依然会疯狂输出“定时器正在运行…”,这非常可怕,你可贵的内存会被一个黏在这个毫无用处的计时器上
而且解绑全局事件监听也是在这个部分很常用的内容,很多时候我们需要监听窗口大小变化 (resize) 来做响应式布局,或者监听键盘事件 (keydown)。这些事件是绑定在 window 对象上的,但是 Vue 组件销毁时,window 对象可不会自动解绑事件。
1 | <script setup> |
addEventListener和removeEventListener的第二个参数(函数引用)必须完全一致,所以不能直接写匿名函数window.addEventListener('resize', () => {}),否则无法移除。
destroyed
对应 Vue3 的 onUnmounted
这个函数就不如上面的实用,因为它的执行时机太晚了,此时组件已经被卸载完毕了。
- DOM 元素已被移除。
- 响应式连接已被断开。
- 所有子组件也都被卸载了。
一般情况下,onBeforeUnmount
已经够用了。但是这个函数依旧可以用于告知外部系统“我已下线”,或进行一些不需要访问实例的收尾工作。
销毁也涉及到一个父子组件的销毁顺序问题
当父组件被销毁时,它必须先把肚子里的“孩子”都清理干净,自己才能安心销毁。
由外向内开始,由内向外结束。
- Parent
beforeUnmount(父组件接到拆迁通知) - Child
beforeUnmount(子组件接到拆迁通知) - Child
unmounted(子组件拆除完毕) - Parent
unmounted(父组件拆除完毕)
Vue3一些额外的钩子函数
onErrorCaptured()
在 Vue 2 中这个钩子用得比较少(通常用全局错误处理),但在 Vue 3 中,随着组件化程度加深,它成为了构建健壮应用的核心工具,类似于 React 中的 Error Boundaries(错误边界)。
顾名思义,这个函数当后代组件(子组件、孙组件…)发生错误时触发。
能捕获哪些错误:
- 组件渲染错误。
- 事件处理器中的错误(v-on)。
- 生命周期钩子里的错误。
- setup() 函数中的错误。
- 侦听器(watchers)中的错误。
它不属于线性的生命周期,它是一个事件监听者。它像一张网,张在父组件上,专门兜住下面掉下来的“错误”。
如果没有这个钩子,子组件一旦报错(比如读取了 undefined 的属性),整个 Vue 应用通常会挂掉(白屏)或者错误直接抛到控制台,用户体验极差。
有了它,父组件可以捕获子组件的错误,并展示一个“出错了”的友好界面,而不是让整个页面崩溃。
所以,在这里,我们就可以对网页设置一个良好的错误返回和报告
这个函数的 API 如下
1 | onErrorCaptured((err, instance, info) => { |
假设我们有一个商品列表,其中某个商品组件因为数据问题报错了,我们希望只让那个商品显示“加载失败”,而不是整个列表消失。
子组件 (BadComponent.vue) —— 故意制造错误:
1
2
3
4
5
6
7
8
9
10<template>
<div class="card">
<!-- ❌ 这里 data 是 undefined,读取 data.name 会报错 -->
{{ data.name }}
</div>
</template>
<script setup>
const props = defineProps(['data']); // 假设父组件传了个 null 进来
</script>父组件 (Parent.vue) —— 错误捕获者:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35<template>
<div class="container">
<h2>商品列表</h2>
<!-- 如果出错,显示备用 UI -->
<div v-if="hasError" class="error-box">
⚠️ 某个商品加载失败,请刷新重试。
</div>
<!-- 如果没出错,显示正常组件 -->
<BadComponent v-else :data="null" />
</div>
</template>
<script setup>
import { ref, onErrorCaptured } from 'vue';
import BadComponent from './BadComponent.vue';
const hasError = ref(false);
// ✅ 捕获子组件的错误
onErrorCaptured((err, instance, info) => {
console.log('捕获到错误:', err);
console.log('错误来源:', info);
// 1. 切换 UI 状态
hasError.value = true;
// 2. 发送错误日志到服务器 (埋点)
// logErrorToService(err);
// 3. 返回 false,阻止错误继续向上传播(防止控制台报红或触发全局错误处理)
return false;
});
</script>
注意,onErrorCaptured 只能抓子孙组件:它抓不到自己 setup 里的错误。自己的错误通常由父组件去抓,或者用全局的 app.config.errorHandler 处理。
onActivated() 和
onDeactivated()
这两个钩子是 专属 于 <KeepAlive>
组件的。
如果你没有使用 <KeepAlive>
包裹组件,这两个钩子永远不会被触发。
所以什么是<KeepAlive>
默认情况下,Vue 组件切换(如 Tab
切换、路由切换)时,旧组件会被销毁(Unmounted),新组件会被重新创建(Mounted)。
- 缺点: 之前的输入内容、滚动条位置、网络请求结果都会丢失。
- 解决: 使用
<KeepAlive>包裹组件,切换时,组件不会销毁,而是进入“缓存(休眠)”状态;再次回来时,直接从缓存恢复。
| 场景 | 普通组件流程 | KeepAlive 组件流程 |
|---|---|---|
| 首次进入 | onMounted | onMounted ->
onActivated |
| 离开页面 | onUnmounted | onDeactivated (休眠,不销毁) |
| 再次进入 | onMounted (重新创建) | onActivated (唤醒,不创建) |
所以这两个钩子函数就是如下这样
- onActivated (激活)
- 触发时机: 组件首次挂载时触发,以及每次从缓存中被重新插入 DOM 时触发。
- 用途:
- 恢复滚动条位置。
- 检查数据是否太旧,决定是否在后台静默刷新数据。
- 开启定时器(如果在休眠时关掉了)。
- onDeactivated (失活)
- 触发时机: 组件被移除(切换走)但因为
还在内存中时触发。 - 用途:
- 暂停耗性能的操作(如复杂的 Canvas 动画、视频播放)。
- 保存当前的 UI 状态(如用户填了一半的表单、滚动位置)。
- 触发时机: 组件被移除(切换走)但因为
不要在 onActivated 里重复 onMounted
的逻辑:如果你把初始化请求写在 onActivated 里,那你用
例如,我们使用这两个钩子函数去做 Tab 切换与数据保持
我们制作一个“新闻列表”页,用户滚到了第 50 条新闻。点去“个人中心”再回来,我们希望列表还停留在第 50 条,而不是重新刷新回到顶部。
1 | <template> |
Home.vue (新闻列表页):
1 | <template> |
- 打开页面: 控制台输出 Mounted -> Activated。
- 点“个人中心”: 控制台输出 Deactivated(注意:没有 Unmounted)。
- 点回“首页”: 控制台输出 Activated(注意:没有 Mounted,时间戳也不会变,说明没重新创建)。





