Vue的生命周期阶段

Vue 组件的生命周期指的是一个组件从创建挂载更新销毁的整个过程。

Vue 在这个过程中的不同阶段会自动调用特定的函数,这些函数被称为生命周期钩子(Lifecycle Hooks),你可以在这些钩子中编写代码来执行特定操作。

Vue 组件的生命周期主要分为四个大阶段,每个阶段包含若干具体的钩子函数

避免使用箭头函数:定义生命周期钩子时,不要使用箭头函数,因为这会导致无法通过 this 获取组件实例。

image-20251130133508264

创建阶段 (Creation)

这个阶段发生在组件实例被创建之后、挂载到 DOM 之前。

  • beforeCreate: 实例刚被创建,数据观测和事件配置之前被调用。此时无法访问 datamethods 等。
  • created: 实例已创建完成,数据观测、属性和方法的运算、事件回调已配置。可以访问 datamethods,但 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)事件 / 生命周期钩子的初始化之前。
  • 可访问性:此时无法访问 datamethodscomputedwatch 等实例属性和方法。this 上下文虽然存在,但指向的实例还未完全初始化。
    • 在这个阶段,数据是获取不到的,并且真实dom元素也是没有渲染出来的
  • 典型用途:几乎很少使用,因为此时实例还未准备好。偶尔用于添加一些非常早期的全局事件监听。

这个例子可以对比 beforeCreatecreated 的访问能力

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
<template>
<div>{{ message }}</div>
</template>

<script>
export default {
data() {
return {
message: 'Hello Vue!'
}
},

methods: {
greet() {
return 'Hello from method!';
}
},

beforeCreate() {
console.log('=== beforeCreate 阶段 ===');
console.log('访问 data:', this.message); // undefined
console.log('访问 method:', this.greet); // undefined
console.log('访问 $el:', this.$el); // undefined
},

created() {
console.log('=== created 阶段 ===');
console.log('访问 data:', this.message); // Hello Vue!
console.log('访问 method:', this.greet()); // Hello from method!
console.log('访问 $el:', this.$el); // undefined
}
}
</script>

created

Vue3写到 setup()

created 是创建阶段的第二个,也是最重要的钩子函数之一。

  • 触发时机:组件实例已经完成了数据观测、属性和方法的运算、watch/event 事件回调的配置之后。
  • 可访问性:此时可以访问 datamethodscomputedwatch 等,但 DOM 还未生成,挂载阶段还没开始,$el 属性尚不存在。
    • 也就是说,在这个阶段,可以访问到数据了,但是页面当中真实dom节点还是没有渲染出来,在这个钩子函数里面,可以进行相关初始化事件的绑定、发送请求操作
  • 典型用途:这是进行数据初始化调用异步 API 获取数据、设置监听事件的最佳时机之一。

例如,我们可以这样在 created 中设置定时器和事件监听

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
36
37
38
39
40
41
42
43
<template>
<div>
<p>计数器: {{ count }}</p>
<button @click="stopCounting">停止计数</button>
</div>
</template>

<script>
export default {
data() {
return {
count: 0,
timer: null
}
},

created() {
// 设置定时器
this.timer = setInterval(() => {
this.count++;
}, 1000);

// 设置全局事件监听
window.addEventListener('resize', this.handleResize);
},

beforeDestroy() {
// 清理资源,避免内存泄漏
clearInterval(this.timer);
window.removeEventListener('resize', this.handleResize);
},

methods: {
stopCounting() {
clearInterval(this.timer);
},

handleResize() {
console.log('Window resized:', window.innerWidth);
}
}
}
</script>

挂载阶段的钩子函数

阶段预览

在 created 钩子执行之后,挂载阶段随即开始。Vue 内部会执行以下逻辑:

  1. 编译模板(Compiler): 检查是否有 template 选项或 el 选项(对于根实例)。Vue 将模板编译成 渲染函数(render function)
  2. 触发 beforeMount: 在渲染开始之前调用。
  3. 执行渲染与打补丁(Render & Patch):
    • 调用 render 函数生成 虚拟 DOM (VNode)
    • Vue 根据虚拟 DOM 生成真实的 DOM 节点。
    • 将真实的 DOM 节点替换插入到页面中对应的挂载点(el)。
  4. 触发 mounted: 真实 DOM 渲染完毕后调用。

beforeMount

Vue3对应onBeforeMount()

在 beforeMount 中,组件已经完成了响应式状态的设置,data, methods, computed 都已可用,而且此时,HTML 模板已经编译好了,但尚未渲染到浏览器页面上。

但是此时

  • 无法获取到真实的 DOM 节点。
  • this.$el (Vue 2) 或模板中的 ref 此时都还不存在或不可用。

这个生命周期钩子函数非常少用。大多数数据初始化的逻辑通常在 created 中进行。在极少数情况下,你可能需要在渲染前最后一次修改数据,且不希望触发额外的重新渲染流程,才会在此时操作。

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
<template>
<div ref="myBox" class="box">
{{ message }}
</div>
</template>

<script setup>
import { ref, onBeforeMount, onMounted } from 'vue';

const message = ref('Hello Vue 3!');
const myBox = ref(null); // 对应模板中的 ref="myBox"

console.log('Setup (相当于 created)');

onBeforeMount(() => {
console.log('--- onBeforeMount ---');
// 这里 myBox.value 还是 null
});

onMounted(() => {
console.log('--- onMounted ---');
// 这里 myBox.value 是真实的 DOM 元素
if (myBox.value) {
myBox.value.style.backgroundColor = '#f0f0f0';
console.log('DOM 宽度:', myBox.value.offsetWidth);
}
});
</script>

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
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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<template>
<div>
<h2>网站访问量统计</h2>
<!-- 为图表准备一个容器,必须有宽高 -->
<div style="width: 800px; height: 400px;">
<canvas id="visitorChart"></canvas>
</div>
</div>
</template>

<script>
// 导入 Chart.js 库
import Chart from 'chart.js/auto';

export default {
data() {
return {
// 存储图表实例,以便后续更新或销毁
chartInstance: null,
// 模拟的图表数据
chartData: {
labels: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
datasets: [{
label: '网站访问量',
data: [120, 200, 150, 80, 70, 110, 130],
backgroundColor: 'rgba(54, 162, 235, 0.5)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1
}]
}
};
},

mounted() {
// 在 mounted 钩子中初始化图表
this.initChart();
},

beforeDestroy() {
// 在组件销毁前销毁图表实例,防止内存泄漏
if (this.chartInstance) {
this.chartInstance.destroy();
}
},

methods: {
initChart() {
// 获取 DOM 元素(此时 DOM 已挂载,可以安全访问)
const ctx = document.getElementById('visitorChart').getContext('2d');

// 初始化 Chart.js 实例
this.chartInstance = new Chart(ctx, {
type: 'bar', // 图表类型:柱状图
data: this.chartData,
options: {
responsive: true,
maintainAspectRatio: false, // 禁用宽高比,使用容器的尺寸
scales: {
y: {
beginAtZero: true,
title: {
display: true,
text: '访问人数'
}
},
x: {
title: {
display: true,
text: '日期'
}
}
}
}
});
},

// 更新图表数据的方法
updateChartData(newData) {
if (this.chartInstance) {
this.chartInstance.data.datasets[0].data = newData;
this.chartInstance.update();
}
}
}
};
</script>
  • mounted 钩子:这是初始化的关键时机。此时组件已经挂载到 DOM 树上,document.getElementById('visitorChart') 可以获取到真实的 DOM 元素,第三方库才能正确找到渲染目标。
  • 存储实例引用:将创建的图表实例存储在 data 中(chartInstance),便于后续操作(如更新数据、销毁实例)。
  • 清理工作:在 beforeDestroy 钩子中调用图表实例的 destroy() 方法,释放资源,避免内存泄漏。

父子组件的挂载顺序

当父组件包含子组件时,挂载阶段的执行顺序是 “由外向内创建,由内向外挂载”

假设结构是:Parent -> Child

  1. Parent beforeCreate
  2. Parent created
  3. Parent beforeMount (父组件准备挂载,解析模板发现有子组件)
  4. Child beforeCreate
  5. Child created
  6. Child beforeMount
  7. Child mounted (子组件先挂载完成)
  8. Parent mounted (父组件最后挂载完成)

结论: 在父组件的 mounted 中,你可以放心地通过 $refs 访问子组件,因为此时子组件一定已经挂载好了。

更新阶段的钩子函数

阶段预览

更新阶段(Update Phase) 是 Vue 生命周期中最“繁忙”的阶段。与挂载阶段只执行一次不同,更新阶段会随着用户操作、接口请求导致的数据变化而反复执行

简单来说,只要页面上用的响应式数据变了,Vue 就会执行:数据变了 -> 重新计算 -> 重新渲染 DOM

当组件的 响应式数据(data/props/computed) 发生变更,且这些数据正在被模版(Template)使用时,更新流程就会触发:

  1. 数据变更: 你执行了 this.message = 'New Value'
  2. 触发 beforeUpdate: 数据已经变了,但 DOM 还没变。
  3. Virtual DOM 重新渲染与 Patch: Vue 内部计算新旧虚拟 DOM 的差异(Diff 算法)。
  4. 真实 DOM 更新: 将差异应用到浏览器页面上。
  5. 触发 updated: 此时页面已经显示了最新的数据。

beforeUpdate

这是更新前的钩子函数,此时数据已经更新了,但 DOM 还在“排队”等待更新。

那么此时页面上的状态有这样的特质:数据是新的,渲染的视图和页面还是旧的(显示修改前的数据或者状态)

那么这个时候就非常适合在 DOM 更新前访问现有的 DOM。比如:获取更新前的滚动条位置,或者手动移除已添加的事件监听器。也就是在现有 DOM 将要被更新之前访问它

Vue 3 对应: onBeforeUpdate

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
<template>
<div class="box">
<p id="count-text">当前计数:{{ count }}</p>
<button @click="count++">点击 +1</button>
</div>
</template>

<script setup>
import { ref, onBeforeUpdate, onUpdated } from 'vue';

const count = ref(0);

// 1. 更新前:数据变了,DOM 没变
onBeforeUpdate(() => {
const domText = document.getElementById('count-text').innerText;
console.log('--- onBeforeUpdate ---');
console.log('JS中的新数据:', count.value); // 输出:1 (新)
console.log('页面上的旧文本:', domText); // 输出:当前计数:0 (旧)
});

// 2. 更新后:数据变了,DOM 也变了
onUpdated(() => {
const domText = document.getElementById('count-text').innerText;
console.log('--- onUpdated ---');
console.log('JS中的新数据:', count.value); // 输出:1 (新)
console.log('页面上的新文本:', domText); // 输出:当前计数:1 (新)
});
</script>
  • 你会发现 onBeforeUpdate 就像是一个快照,它保留了页面变身前的最后一刻。

当父组件数据变化,通过 props 传给子组件时,更新顺序是怎样的?

  1. Parent beforeUpdate
  2. Child beforeUpdate
  3. Child updated (子组件先更新完 DOM)
  4. Parent updated (父组件最后更新完)

“父先准备,子先完成,父最后完成”

updated

Vue 3 对应: onUpdated

这就是更新后的钩子函数了,执行它的时候,DOM 已经根据最新的数据重新渲染完毕。此时页面上的数据是新的,视图也是新的

这个函数非常常用,例如当数据变化导致视图变化后,需要执行依赖于新 DOM 的操作。

在类似微信的聊天界面中,当发送新消息(列表数据更新)后,滚动条应该自动跳到最底部,让用户看到最新消息。

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
36
37
38
39
<template>
<div>
<!-- 聊天记录容器 -->
<div ref="chatBox" class="chat-container">
<div v-for="(msg, index) in messages" :key="index" class="message">
{{ msg }}
</div>
</div>
<button @click="sendMessage">发送消息</button>
</div>
</template>

<script setup>
import { ref, onUpdated } from 'vue';

const messages = ref(['你好', '欢迎']);
const chatBox = ref(null);

const sendMessage = () => {
messages.value.push(`新消息 ${messages.value.length + 1}`);
};

// 每次视图更新后(即消息渲染到页面后),执行滚动逻辑
onUpdated(() => {
// 获取 DOM 元素
const el = chatBox.value;
// 设置滚动条位置 = 内容的总高度 (即滚到底部)
el.scrollTop = el.scrollHeight;
console.log('视图已更新,已自动滚动到底部');
});
</script>

<style>
.chat-container {
height: 100px;
overflow-y: auto; /* 必须有滚动条 */
border: 1px solid #ccc;
}
</style>

那么这里为什么不用 watch,如果你用 watch 监听 messages,代码执行时数据变了,但 DOM 还没渲染新消息。此时获取的 scrollHeight 还是旧的高度,滚动条会滚到倒数第二条消息的位置。只有在 updated 中,DOM 才是包含最新消息的状态。

updated vs this.$nextTick (或 nextTick)

这两个确实很像,但是通常情况下不能混用

  • updated:是一个全局的钩子。只要该组件内任何响应式数据变化导致视图更新,它都会触发。如果你的组件很复杂,里面有几十个变量,其中一个变了,updated 就会跑一次,这可能导致性能浪费或逻辑混乱。
  • nextTick:通常结合具体逻辑使用。例如我只想在这次修改数据后,等待 DOM 更新做某事。

如果你只想针对特定操作(比如点击发送按钮)后的 DOM 更新做处理,请使用 nextTick,而不是在 updated 里写大段的 if…else 判断。

所以我上面的例子只是一个例子,事实上没有人会这样写

注意,千万不要尝试在 updated 钩子中去修改数据,这是非常危险的操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<script setup>
import { ref, onUpdated } from 'vue';

const count = ref(0);

onUpdated(() => {
console.log('视图更新了');

// ❌ 致命错误!
// 逻辑:如果 count 小于 10,就加 1
if (count.value < 10) {
count.value++;
}
});
</script>
  1. 你修改了 count。
  2. 触发 updated。
  3. 在 updated 里,你又执行了 count.value++。
  4. 数据变了 -> 再次触发更新 -> 再次触发 updated -> 再次 count++…
  5. 结果: 浏览器卡死(Infinite Loop),或者 Vue 报错提示检测到无限递归。

销毁阶段的钩子函数

阶段预览

这个阶段的函数都在实例销毁后调用。该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁。

在 Vue 2 中,这个阶段被称为 Destroy;在 Vue 3 中,为了语义更准确,改名为 Unmount(卸载)。虽然名字变了,但核心逻辑是一样的:清理资源,防止内存泄漏

什么时候会触发销毁?

组件并不是随时都会被销毁的。以下情况会触发销毁流程:

  1. 条件渲染: 使用 v-if="false"(注意:v-show 只是隐藏,不会销毁)。
  2. 路由切换: 切换页面(Vue Router)时,老页面的组件通常会被销毁。
  3. 父组件销毁: 覆巢之下无完卵,父组件被移除,子组件也会随之销毁。
  4. 手动调用: 调用 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
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
<template>
<div>
<h3>倒计时:{{ count }}</h3>
<button @click="stopTimer">手动停止</button>
</div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';

const count = ref(100);
let timerId = null; // 用来保存定时器 ID

onMounted(() => {
// 开启定时器
timerId = setInterval(() => {
count.value--;
console.log('定时器正在运行...', count.value);
}, 1000);
});

// ✅ 正确做法:组件销毁前,务必清除定时器
onBeforeUnmount(() => {
if (timerId) {
clearInterval(timerId);
timerId = null;
console.log('--- 组件销毁,定时器已清除 ---');
}
});
</script>

如果注释掉 onBeforeUnmount 里的代码,当你切换路由去别的页面时,控制台依然会疯狂输出“定时器正在运行…”,这非常可怕,你可贵的内存会被一个黏在这个毫无用处的计时器上

而且解绑全局事件监听也是在这个部分很常用的内容,很多时候我们需要监听窗口大小变化 (resize) 来做响应式布局,或者监听键盘事件 (keydown)。这些事件是绑定在 window 对象上的,但是 Vue 组件销毁时,window 对象可不会自动解绑事件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';

// 定义事件处理函数
const handleResize = () => {
console.log('窗口大小改变了:', window.innerWidth);
};

onMounted(() => {
// 绑定全局事件
window.addEventListener('resize', handleResize);
});

// ✅ 正确做法:离开组件时,必须移除监听
// 如果不移除,用户在其他页面调整窗口大小时,这个函数依然会报错执行
onBeforeUnmount(() => {
window.removeEventListener('resize', handleResize);
console.log('--- 组件销毁,Resize 监听已移除 ---');
});
</script>
  • addEventListenerremoveEventListener 的第二个参数(函数引用)必须完全一致,所以不能直接写匿名函数 window.addEventListener('resize', () => {}),否则无法移除。

destroyed

对应 Vue3 的 onUnmounted

这个函数就不如上面的实用,因为它的执行时机太晚了,此时组件已经被卸载完毕了。

  • DOM 元素已被移除。
  • 响应式连接已被断开。
  • 所有子组件也都被卸载了。

一般情况下,onBeforeUnmount 已经够用了。但是这个函数依旧可以用于告知外部系统“我已下线”,或进行一些不需要访问实例的收尾工作。

销毁也涉及到一个父子组件的销毁顺序问题

当父组件被销毁时,它必须先把肚子里的“孩子”都清理干净,自己才能安心销毁。

由外向内开始,由内向外结束。

  1. Parent beforeUnmount (父组件接到拆迁通知)
  2. ChildbeforeUnmount (子组件接到拆迁通知)
  3. Child unmounted (子组件拆除完毕)
  4. Parent unmounted (父组件拆除完毕)

Vue3一些额外的钩子函数

onErrorCaptured()

在 Vue 2 中这个钩子用得比较少(通常用全局错误处理),但在 Vue 3 中,随着组件化程度加深,它成为了构建健壮应用的核心工具,类似于 React 中的 Error Boundaries(错误边界)

顾名思义,这个函数当后代组件(子组件、孙组件…)发生错误时触发。

能捕获哪些错误

  • 组件渲染错误。
  • 事件处理器中的错误(v-on)。
  • 生命周期钩子里的错误。
  • setup() 函数中的错误。
  • 侦听器(watchers)中的错误。

它不属于线性的生命周期,它是一个事件监听者。它像一张网,张在父组件上,专门兜住下面掉下来的“错误”。

如果没有这个钩子,子组件一旦报错(比如读取了 undefined 的属性),整个 Vue 应用通常会挂掉(白屏)或者错误直接抛到控制台,用户体验极差。

有了它,父组件可以捕获子组件的错误,并展示一个“出错了”的友好界面,而不是让整个页面崩溃。

所以,在这里,我们就可以对网页设置一个良好的错误返回和报告

这个函数的 API 如下

1
2
3
4
5
6
7
8
onErrorCaptured((err, instance, info) => {
// err: 错误对象
// instance: 发生错误的组件实例
// info: 错误的类型字符串(例如 'render function')

// 返回 false 表示:错误已经被我处理了,不要再往上抛给全局的 errorHandler 了
return false;
})

假设我们有一个商品列表,其中某个商品组件因为数据问题报错了,我们希望只让那个商品显示“加载失败”,而不是整个列表消失。

  • 子组件 (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 里,那你用 就没意义了,因为每次切回来都会重新请求。正确的做法是 onMounted 初始化,onActivated 做轻量级检查。

例如,我们使用这两个钩子函数去做 Tab 切换与数据保持

我们制作一个“新闻列表”页,用户滚到了第 50 条新闻。点去“个人中心”再回来,我们希望列表还停留在第 50 条,而不是重新刷新回到顶部。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<!-- ✅ 使用 KeepAlive 包裹动态组件 -->
<KeepAlive>
<component :is="currentTab" />
</KeepAlive>

<button @click="currentTab = Home">首页</button>
<button @click="currentTab = Profile">个人中心</button>
</template>

<script setup>
import { shallowRef } from 'vue';
import Home from './Home.vue';
import Profile from './Profile.vue';

const currentTab = shallowRef(Home);
</script>

Home.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
<template>
<div class="news-list">
<h3>新闻列表 (上次更新: {{ lastUpdateTime }})</h3>
<ul>
<li v-for="n in 50" :key="n">新闻条目 {{ n }}</li>
</ul>
</div>
</template>

<script setup>
import { ref, onMounted, onActivated, onDeactivated } from 'vue';

const lastUpdateTime = ref('');

// 1. 只在组件第一次创建时执行一次
onMounted(() => {
console.log('--- Home 组件被创建 (Mounted) ---');
lastUpdateTime.value = new Date().toLocaleTimeString();
// 发送网络请求获取数据...
});

// 2. 每次切回来都会执行
onActivated(() => {
console.log('--- Home 组件被唤醒 (Activated) ---');
// 可以在这里判断:如果数据超过 1 小时没更新,才重新请求
// 否则直接使用缓存的数据,无需任何操作,用户体验极佳
});

// 3. 每次切走都会执行
onDeactivated(() => {
console.log('--- Home 组件进入休眠 (Deactivated) ---');
// 比如:记录当前滚动条位置,或者暂停视频播放
});
</script>
  1. 打开页面: 控制台输出 Mounted -> Activated。
  2. 点“个人中心”: 控制台输出 Deactivated(注意:没有 Unmounted)。
  3. 点回“首页”: 控制台输出 Activated(注意:没有 Mounted,时间戳也不会变,说明没重新创建)。