Vue 组件的生命周期是其核心概念之一,它描述了一个组件从创建、挂载、更新到销毁的完整过程。理解生命周期对于编写可预测、高性能的 Vue 应用至关重要。本文将深入探讨 Vue3 的生命周期,并结合组件间通信(Props、Emits、Provide/Inject)来展示它们在实际开发中的协同工作方式。
1. 什么是生命周期?
想象一个组件就像一个有生命的物体:
- 创建 (Creation):它在代码中被定义,初始化其数据和状态。
- 挂载 (Mounting):它被渲染到浏览器的 DOM 中,用户可以看到它。
- 更新 (Updating):当它的内部数据或外部传入的 props 发生变化时,它会重新渲染,以反映最新的状态。
- 销毁 (Unmounting):它从 DOM 中被移除,清理所有占用的资源。
生命周期钩子(Lifecycle Hooks)就是在这些关键时间点被自动调用的函数,让开发者有机会在组件的不同阶段执行自定义逻辑。
2. Vue3 中的生命周期钩子
Vue3 提供了两种使用生命周期钩子的方式:选项式 API (Options API) 和 组合式 API (Composition API)。
2.1 选项式 API 生命周期
如果你习惯 Vue2 的写法,选项式 API 会非常亲切。你可以直接在组件的选项对象中定义钩子函数。
vue
<script>
export default {
name: 'LifecycleDemo',
data() {
return {
message: 'Hello, Vue!'
}
},
// 1. 创建阶段
beforeCreate() {
console.log('beforeCreate: 实例已初始化,但数据观测和事件/侦听器尚未设置。');
console.log('此时 this.message:', this.message); // undefined
},
created() {
console.log('created: 实例创建完成。数据观测、计算属性、方法、事件/侦听器已设置。');
console.log('此时 this.message:', this.message); // 'Hello, Vue!'
// 可以在这里发起异步请求,但不能访问 DOM
},
// 2. 挂载阶段
beforeMount() {
console.log('beforeMount: 模板编译/渲染函数已生成,但尚未挂载到 DOM。');
console.log('此时 $el:', this.$el); // undefined
},
mounted() {
console.log('mounted: 组件已挂载到 DOM。');
console.log('此时 $el:', this.$el); // <div ...>...</div>
// **非常重要的钩子**,可以在这里进行 DOM 操作、初始化非 Vue 插件等
},
// 3. 更新阶段
beforeUpdate() {
console.log('beforeUpdate: 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。');
// 适合在更新前访问现有的 DOM,比如手动移除已添加的事件监听器
},
updated() {
console.log('updated: 由于数据更改导致的虚拟 DOM 重新渲染和打补丁完成。');
// DOM 已经更新完毕
// **注意**:不要在此钩子中修改数据,否则可能导致无限循环的更新!
},
// 4. 销毁阶段
beforeUnmount() {
console.log('beforeUnmount: 组件实例销毁之前。');
// 实例仍然完全可用。适合在这里清理定时器、解绑全局事件、销毁第三方库实例等。
},
unmounted() {
console.log('unmounted: 组件实例销毁之后。');
// 所有指令的绑定、事件监听器和子组件实例都已被移除。
}
}
</script>
2.2 组合式 API 生命周期
组合式 API 提供了更灵活的代码组织方式。它将生命周期钩子作为独立的函数从 vue
包中导入。
选项式 API | 组合式 API (钩子) | 描述 |
---|---|---|
beforeCreate |
- | 在 setup() 中,逻辑会自动执行于此阶段。 |
created |
- | 在 setup() 中,逻辑会自动执行于此阶段。 |
beforeMount |
onBeforeMount |
组件挂载前调用。 |
mounted |
onMounted |
组件挂载后调用。 |
beforeUpdate |
onBeforeUpdate |
组件更新前调用。 |
updated |
onUpdated |
组件更新后调用。 |
beforeUnmount |
onBeforeUnmount |
组件卸载前调用。 |
unmounted |
onUnmounted |
组件卸载后调用。 |
使用示例:
vue
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue';
const message = ref('Hello, Vue!');
console.log('setup: 相当于 beforeCreate 和 created 的组合。');
onBeforeMount(() => {
console.log('onBeforeMount: 组件即将挂载。');
});
onMounted(() => {
console.log('onMounted: 组件已挂载。');
// 适合进行 DOM 操作
});
onBeforeUpdate(() => {
console.log('onBeforeUpdate: 组件即将更新。');
});
onUpdated(() => {
console.log('onUpdated: 组件已更新。');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount: 组件即将卸载。');
// 适合清理工作
});
onUnmounted(() => {
console.log('onUnmounted: 组件已卸载。');
});
</script>
为什么 setup()
取代了 beforeCreate
和 created
? 因为 setup()
是在组件实例创建之初、所有选项式 API 钩子之前执行的,所以它天然地包含了 beforeCreate
和 created
的功能。你所有在这两个钩子中想做的事(如数据初始化、发起 API 请求)都应该直接写在 setup()
中或其顶层。
3. 生命周期执行顺序:父子组件
当组件嵌套时,它们的生命周期钩子会按特定顺序执行。这对于理解数据流和组件交互至关重要。
挂载 (Mounting) 顺序:
- 父组件
beforeCreate
- 父组件
created
- 父组件
beforeMount
- 子组件
beforeCreate
- 子组件
created
- 子组件
beforeMount
- 子组件
mounted
- 父组件
mounted
结论: 父组件先准备好,然后子组件完成挂载,最后父组件才宣告挂载完成。这确保了父组件可以在 mounted
钩子中安全地访问已挂载的子组件。
更新 (Updating) 顺序:
- 父组件
beforeUpdate
- 子组件
beforeUpdate
- 子组件
updated
- 父组件
updated
结论: 父组件先进入更新准备,子组件完成更新后,父组件才完成自己的更新。
卸载 (Unmounting) 顺序:
- 父组件
beforeUnmount
- 子组件
beforeUnmount
- 子组件
unmounted
- 父组件
unmounted
结论: 父组件先准备卸载,然后子组件被完全卸载,最后父组件才完成卸载。这给了父组件在 beforeUnmount
中处理子组件相关逻辑的机会。
4. 组件通信与生命周期的结合
组件通信是 Vue 应用的基石,而生命周期则决定了在何时进行通信是安全和有效的。
4.1 父传子 (Props)
父组件通过 props
将数据传递给子组件。
关键点: 子组件的 props
在 created
、beforeMount
和 mounted
钩子中都是可用的。
场景: 子组件需要根据父组件传入的 id
来从服务器加载数据。
父组件 (Parent.vue
)
vue
<template>
<ChildComponent :user-id="userId" />
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const userId = ref(123);
</script>
子组件 (ChildComponent.vue
)
vue
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
userId: {
type: Number,
required: true
}
});
const userData = ref(null);
// 当组件挂载后,使用 props 中的 userId 发起请求
onMounted(async () => {
console.log('子组件 mounted, userId is:', props.userId); // 123
try {
const response = await fetch(`/api/users/${props.userId}`);
userData.value = await response.json();
} catch (error) {
console.error('Failed to fetch user data:', error);
}
});
</script>
分析: 在子组件的 onMounted
钩子中,我们可以确信 props.userId
已经被父组件传入并可用,因此在这里发起 API 请求是安全的。
4.2 子传父 (Emits)
子组件通过 emits
触发事件,父组件监听这些事件来接收数据。
关键点: 子组件可以在任何生命周期钩子中触发事件,但通常是在某个用户交互或异步操作完成后。
场景: 子组件有一个按钮,点击后通知父组件更新一个值。
子组件 (ChildComponent.vue
)
vue
<template>
<button @click="handleClick">Click Me</button>
</template>
<script setup>
import { defineEmits } from 'vue';
const emit = defineEmits(['child-click']);
const handleClick = () => {
console.log('子组件按钮被点击,准备触发事件。');
emit('child-click', 'Hello from Child!');
};
</script>
父组件 (Parent.vue
)
vue
<template>
<div>
<p>Message from child: {{ messageFromChild }}</p>
<ChildComponent @child-click="handleChildClick" />
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const messageFromChild = ref('');
const handleChildClick = (message) => {
console.log('父组件收到子组件事件:', message);
messageFromChild.value = message;
};
</script>
分析: 事件的触发和监听是即时的,与特定的生命周期钩子没有强绑定。但父组件对事件的响应(如更新自身数据)会触发父组件的 beforeUpdate
和 updated
钩子。
4.3 跨层级通信 (Provide / Inject)
Provide / Inject
用于解决深层嵌套组件间的通信问题,避免了 props
的"层层透传"。
关键点: provide
的数据在子组件的 setup
、created
、mounted
等钩子中都可以通过 inject
访问到。
场景: 祖父组件提供一个主题色,孙子组件注入并使用它。
祖父组件 (Grandparent.vue
)
vue
<script setup>
import { provide, ref } from 'vue';
const theme = ref('dark');
// 在 setup 中提供数据
provide('appTheme', theme);
</script>
孙子组件 (Grandchild.vue
)
vue
<script setup>
import { inject, onMounted } from 'vue';
// 在 setup 中注入数据
const theme = inject('appTheme');
onMounted(() => {
console.log('孙子组件 mounted, theme is:', theme.value); // 'dark'
// 可以根据 theme 值来设置 DOM 样式
});
</script>
分析: inject
可以在 setup
的任何地方使用,包括生命周期钩子。这使得深层组件能够轻松地访问祖先组件提供的数据。
5. 总结与最佳实践
onMounted
是进行 DOM 操作和初始化外部库的首选位置。 此时组件已经被渲染到页面上。onBeforeUnmount
是进行清理工作的最佳位置。 务必在这里清除定时器 (clearInterval
,clearTimeout
)、解绑全局事件监听器 (window.removeEventListener
)、销毁第三方库实例等,以防止内存泄漏。- 避免在
updated
中修改数据。 这极易导致无限循环的更新。如果需要在 DOM 更新后执行某些操作,可以使用nextTick
。 - 理解父子组件的生命周期顺序。 这对于处理依赖于子组件状态的父组件逻辑至关重要。例如,父组件想在
mounted
中调用子组件的方法,必须确保子组件已经mounted
。 setup()
是数据初始化和发起异步请求的好地方。 因为它在所有生命周期钩子之前执行,你可以在这里为组件的整个生命周期准备好响应式数据。- 组件通信与生命周期的结合是自然而然的。 Props 和 Provide 的数据在组件的大部分生命周期中都是可用的,而 Emits 则可以在任何需要通知父级的时刻被触发。