第 120 章:生命周期
学习目标
- 理解 Vue 3 组件的生命周期
- 掌握各个生命周期钩子的使用场景
- 学会在生命周期中处理副作用
- 区分 Composition API 与 Options API 的生命周期
一、生命周期总览
二、生命周期钩子详解
2.1 创建阶段
setup()
vue
<script setup lang="ts">
import { ref } from 'vue';
// setup 在所有生命周期之前执行
// 是 Composition API 的入口
const count = ref(0);
console.log('setup');
// ❌ 不要在 setup 中用 onMounted 等异步副作用 API
// ✅ 同步逻辑可以直接写
</script>onBeforeMount
vue
<script setup lang="ts">
import { onBeforeMount } from 'vue';
onBeforeMount(() => {
// 组件挂载到 DOM 之前
// 此时模板还没渲染,$el 不可用
console.log('组件即将挂载');
});
</script>2.2 挂载阶段
onMounted
vue
<script setup lang="ts">
import { onMounted, ref } from 'vue';
const el = ref<HTMLDivElement | null>(null);
onMounted(() => {
// 组件已挂载到 DOM,$el 可用
console.log('组件已挂载', el.value);
// 适合做:DOM 操作、数据初始化、订阅事件
});
</script>
<template>
<div ref="el">Hello</div>
</template>典型用途:
- 请求初始数据
- DOM 操作(focus、scroll 等)
- 启动定时器
- 订阅事件(window.resize 等)
2.3 更新阶段
onBeforeUpdate
vue
<script setup lang="ts">
import { onBeforeUpdate, ref } from 'vue';
const count = ref(0);
onBeforeUpdate(() => {
// 数据变了,但 DOM 还没更新
console.log('组件即将更新', count.value);
// 适合做:在更新前读取旧 DOM 状态
});
</script>onUpdated
vue
<script setup lang="ts">
import { onUpdated, ref } from 'vue';
const count = ref(0);
onUpdated(() => {
// 数据变了,DOM 也更新了
console.log('组件已更新', count.value);
// 适合做:基于新 DOM 的操作
});
</script>注意
不要在 onUpdated 中修改数据,会再次触发更新,可能导致死循环。
2.4 卸载阶段
onBeforeUnmount
vue
<script setup lang="ts">
import { onBeforeUnmount } from 'vue';
onBeforeUnmount(() => {
// 组件即将卸载
// 组件还可用,可以做最后的清理
console.log('组件即将卸载');
});
</script>onUnmounted
vue
<script setup lang="ts">
import { onUnmounted } from 'vue';
let timer: number;
onUnmounted(() => {
// 组件已卸载
// 清理:定时器、事件监听、订阅
clearInterval(timer);
});
</script>典型用途:
- 清理定时器
- 取消事件监听
- 取消网络请求
- 清理订阅
2.5 其他钩子
onActivated / onDeactivated(KeepAlive)
vue
<script setup lang="ts">
import { onActivated, onDeactivated } from 'vue';
// KeepAlive 包裹的组件激活时
onActivated(() => {
console.log('组件被激活');
});
// 失活时(被缓存)
onDeactivated(() => {
console.log('组件被缓存');
});
</script>onErrorCaptured(错误捕获)
vue
<script setup lang="ts">
import { onErrorCaptured } from 'vue';
onErrorCaptured((err, instance, info) => {
console.error('捕获到子组件错误', err);
// 返回 false 阻止向上传播
return false;
});
</script>onRenderTracked / onRenderTriggered(调试)
vue
<script setup lang="ts">
import { onRenderTracked, onRenderTriggered } from 'vue';
// 依赖被追踪时
onRenderTracked((event) => {
console.log('追踪', event);
});
// 重新渲染被触发时
onRenderTriggered((event) => {
console.log('触发渲染', event);
});
</script>三、Composition API vs Options API 对照
| Options API | Composition API |
|---|---|
beforeCreate | setup() |
created | setup() |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
activated | onActivated |
deactivated | onDeactivated |
3.1 对照示例
typescript
// ❌ Options API
export default {
data() {
return { count: 0 };
},
mounted() {
console.log('mounted');
},
beforeUnmount() {
console.log('beforeUnmount');
}
};
// ✅ Composition API
import { ref, onMounted, onBeforeUnmount } from 'vue';
const count = ref(0);
onMounted(() => console.log('mounted'));
onBeforeUnmount(() => console.log('beforeUnmount'));四、生命周期执行顺序
4.1 单组件
4.2 嵌套组件
vue
<!-- Parent.vue -->
<script setup>
import { onMounted, onBeforeMount, onUnmounted } from 'vue';
import Child from './Child.vue';
onBeforeMount(() => console.log('Parent: beforeMount'));
onMounted(() => console.log('Parent: mounted'));
onUnmounted(() => console.log('Parent: unmounted'));
</script>
<template>
<Child />
</template>vue
<!-- Child.vue -->
<script setup>
import { onMounted, onBeforeMount, onUnmounted } from 'vue';
onBeforeMount(() => console.log('Child: beforeMount'));
onMounted(() => console.log('Child: mounted'));
onUnmounted(() => console.log('Child: unmounted'));
</script>挂载顺序:Parent beforeMount → Child beforeMount → Child mounted → Parent mounted
卸载顺序:Parent beforeUnmount → Child beforeUnmount → Child unmounted → Parent unmounted
五、实战案例
5.1 数据请求
vue
<script setup lang="ts">
import { ref, onMounted } from 'vue';
const users = ref<User[]>([]);
const loading = ref(false);
const error = ref<Error | null>(null);
async function fetchUsers() {
loading.value = true;
try {
const res = await fetch('/api/users');
users.value = await res.json();
} catch (e) {
error.value = e as Error;
} finally {
loading.value = false;
}
}
onMounted(fetchUsers);
</script>
<template>
<div v-if="loading">加载中...</div>
<div v-else-if="error">错误: {{ error.message }}</div>
<ul v-else>
<li v-for="user in users" :key="user.id">{{ user.name }}</li>
</ul>
</template>5.2 定时器
vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const now = ref(new Date());
let timer: number;
function update() {
now.value = new Date();
}
onMounted(() => {
update();
timer = window.setInterval(update, 1000);
});
onUnmounted(() => {
clearInterval(timer);
});
</script>
<template>
<div>{{ now.toLocaleTimeString() }}</div>
</template>5.3 监听事件
vue
<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue';
const width = ref(window.innerWidth);
function handleResize() {
width.value = window.innerWidth;
}
onMounted(() => {
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
});
</script>
<template>
<div>窗口宽度: {{ width }}px</div>
</template>5.4 自动滚动到底部
vue
<script setup lang="ts">
import { ref, watch, nextTick } from 'vue';
const messages = ref<Message[]>([]);
const containerRef = ref<HTMLDivElement | null>(null);
watch(
() => messages.value.length,
async () => {
await nextTick();
if (containerRef.value) {
containerRef.value.scrollTop = containerRef.value.scrollHeight;
}
}
);
</script>
<template>
<div ref="containerRef" class="message-list">
<div v-for="msg in messages" :key="msg.id">{{ msg.text }}</div>
</div>
</template>5.5 自动保存草稿
vue
<script setup lang="ts">
import { ref, watch, onMounted, onUnmounted } from 'vue';
const draft = ref(localStorage.getItem('draft') || '');
let timer: number;
watch(draft, (val) => {
// debounce:延迟 500ms 保存
clearTimeout(timer);
timer = window.setTimeout(() => {
localStorage.setItem('draft', val);
}, 500);
});
onUnmounted(() => {
clearTimeout(timer);
// 最终保存
localStorage.setItem('draft', draft.value);
});
</script>
<template>
<textarea v-model="draft" placeholder="开始输入..." />
</template>六、VueUse 简化副作用
推荐用 VueUse 处理常见的副作用逻辑。
6.1 useEventListener
typescript
import { useEventListener } from '@vueuse/core';
// 自动清理
useEventListener(window, 'resize', () => {
console.log(window.innerWidth);
});6.2 useIntervalFn
typescript
import { useIntervalFn } from '@vueuse/core';
const { pause, resume } = useIntervalFn(() => {
console.log('tick');
}, 1000);
// pause() / resume()6.3 useElementVisibility
typescript
import { useElementVisibility } from '@vueuse/core';
const target = ref<HTMLDivElement | null>(null);
const visible = useElementVisibility(target);
watch(visible, (v) => {
if (v) console.log('元素可见');
});七、常见错误
7.1 在 setup 中用 DOM API
vue
<script setup>
// ❌ 此时 DOM 还没渲染
onMounted(() => {
document.querySelector('.my-el');
});
// ❌ 同步执行,DOM 不存在
const width = window.innerWidth;
</script>7.2 在 onUpdated 修改数据
vue
<script setup>
const count = ref(0);
onUpdated(() => {
// ❌ 会触发死循环
count.value++;
});
</script>7.3 忘记清理副作用
vue
<script setup>
// ❌ 内存泄漏
let timer = setInterval(() => {
console.log('tick');
}, 1000);
// ✅ 必须清理
onUnmounted(() => clearInterval(timer));
</script>八、本章小结
| 生命周期 | 触发时机 | 用途 |
|---|---|---|
| setup | 创建时 | 初始化数据 |
| onBeforeMount | 挂载前 | 极少用 |
| onMounted | 挂载后 | 数据请求、DOM 操作 |
| onBeforeUpdate | 更新前 | 极少用 |
| onUpdated | 更新后 | 基于新 DOM 操作 |
| onBeforeUnmount | 卸载前 | 最后的清理 |
| onUnmounted | 卸载后 | 清理副作用 |
| onActivated | KeepAlive 激活 | 恢复状态 |
| onDeactivated | KeepAlive 失活 | 保存状态 |
动手练习
- 数据请求:实现一个用户列表组件,挂载时请求数据
- 定时器:实现一个秒表组件,挂载启动、卸载停止
- 事件监听:监听
window.resize,实现响应式布局 - KeepAlive:实现一个标签页切换,缓存已访问页面状态
推荐阅读
- 📖 Vue 3 生命周期 — 官方文档
- 📖 Vue 3 生命周期钩子 API — API 文档
- 🌐 VueUse 集合 — 工具库
下一章:第 121 章:setup 与组合式 API →