在 Vue 3 中,组件实例的生命周期是其从创建到销毁的完整旅程。理解这一过程,以及组件实例内部用于跟踪状态的属性(如 _isMounted、_isDestroyed),是深入掌握 Vue 响应式和渲染机制的关键。
一、组件生命周期的宏观流程
一个组件实例的生命周期可以分为三个主要阶段:
- 创建(Creation):实例化、设置响应式、编译模板。
- 挂载与更新(Mounting & Update):插入 DOM、响应数据变化。
- 卸载(Unmounting):从 DOM 移除、清理资源。
Vue 3 的组合式 API 主要通过 setup 和生命周期钩子来介入这一过程。
二、生命周期钩子函数
Vue 提供了一系列生命周期钩子,允许你在特定阶段执行代码。
1. 创建阶段
setup():- 组合式 API 的入口。
- 在
beforeCreate和created之间执行。 - 返回
setup中定义的响应式状态和方法。
beforeCreate(选项式):- 实例初始化后,数据观测 (data observation) 和事件/watcher 配置之前被调用。
created:- 实例创建完成。
- 数据、计算属性、方法、
watch/event回调都已设置。 - 可以访问响应式数据。
- 但尚未挂载到 DOM,
$el为undefined。
2. 挂载阶段
beforeMount:- 挂载开始前被调用。
render函数首次被调用。- 虚拟 DOM 已创建,但尚未生成真实 DOM。
mounted:- 组件已挂载到 DOM。
el属性现在可用。- 可以安全地访问和操作 DOM。
- 适合发起网络请求、设置定时器、集成第三方库。
3. 更新阶段
beforeUpdate:- 响应式数据变化,导致重新渲染之前调用。
- 虚拟 DOM 即将重新渲染。
- DOM 仍是旧的状态。
updated:- 组件因响应式数据变化而重新渲染后调用。
- DOM 已更新。
- 注意:避免在此钩子中修改状态,可能引发无限循环。
4. 卸载阶段
beforeUnmount(原beforeDestroy):- 实例卸载之前调用。
- 组件实例仍然完全正常。
- 清理定时器、取消订阅、移除事件监听器。
unmounted(原destroyed):- 组件实例已从 DOM 中移除。
- 所有指令都被解绑,所有事件监听器被移除,所有子组件实例被卸载。
- 实例不可用。
三、组件实例的内部状态
Vue 内部使用一系列以 _ 开头的属性来跟踪组件实例的当前状态。这些是内部属性,不应在应用代码中直接访问,但理解它们有助于理解生命周期。
1. _isMounted
- 作用:标记组件是否已经挂载到 DOM。
- 初始值:
false。 - 何时变为
true:- 在
mounted钩子执行之后,由 Vue 内部设置为true。
- 在
- 用途:
- 防止在
mounted之前执行某些操作。 - 在
updated钩子中,确保组件已经挂载。 - 响应式系统在更新时检查此状态,避免对未挂载组件进行不必要的操作。
- 防止在
js
// 伪代码
callHook(instance, 'mounted');
instance._isMounted = true;2. _isDestroyed(或 _isUnmounted)
- 作用:标记组件实例是否已被销毁(卸载)。
- 初始值:
false。 - 何时变为
true:- 在
unmounted钩子执行之后,由 Vue 内部设置为true。
- 在
- 用途:
- 防止内存泄漏:在异步操作(如
setTimeout、API 响应)中,检查组件是否已销毁,避免在已卸载的组件上操作。 - 避免无效更新:响应式系统在触发更新前会检查此标志,如果为
true,则跳过更新。
- 防止内存泄漏:在异步操作(如
js
// 伪代码
callHook(instance, 'unmounted');
instance._isDestroyed = true;
// 之后清理引用3. _isDeactivated(用于 <keep-alive>)
- 作用:标记组件是否被
<keep-alive>缓存(失活)。 - 初始值:
false。 - 何时变化:
- 组件被
<keep-alive>包裹时,切换离开时变为true,切换回来时变为false。
- 组件被
- 相关钩子:
onActivated:组件被激活时调用。onDeactivated:组件被缓存(失活)时调用。
4. _vnode
- 作用:指向当前组件的虚拟 DOM 节点(VNode)。
- 用途:
- 渲染过程中用于
patch比较。 - 在更新和卸载时引用当前 VNode。
- 渲染过程中用于
5. _parent 和 _children
- 作用:维护组件树的父子关系。
- 用途:
provide/inject依赖_parent链向上查找。- 卸载时递归卸载所有
_children。
四、生命周期与内部状态的交互
以一个完整的生命周期为例:
js
// 1. 创建
const instance = createComponentInstance(vnode);
// instance._isMounted = false
// instance._isDestroyed = false
// 2. setup & created
setupComponent(instance);
callHook('created');
// 3. 挂载
callHook('beforeMount');
const subTree = instance.render(); // 生成虚拟 DOM
patch(null, subTree, container); // 挂载到 DOM
callHook('mounted');
instance._isMounted = true; // 标记为已挂载
// 4. 更新(数据变化)
if (instance._isMounted && !instance._isDestroyed) {
callHook('beforeUpdate');
const newSubTree = instance.render();
patch(subTree, newSubTree, container);
subTree = newSubTree;
callHook('updated');
}
// 5. 卸载
callHook('beforeUnmount');
unmountChildren(instance.children); // 卸载子组件
remove(instance.vnode.el); // 从 DOM 移除
callHook('unmounted');
instance._isDestroyed = true; // 标记为已销毁
// 清理引用,等待垃圾回收五、内部状态的实际应用(安全实践)
虽然你不应直接访问 _isMounted 或 _isDestroyed,但理解它们能帮助你编写更安全的代码。
1. 避免在已销毁组件上操作
js
import { onMounted, onBeforeUnmount } from 'vue';
export default {
setup() {
let timer = null;
onMounted(() => {
timer = setInterval(() => {
// ❌ 危险:组件可能已销毁
// someAsyncOperation().then(() => this.updateData());
}, 1000);
});
onBeforeUnmount(() => {
clearInterval(timer);
timer = null;
});
// ✅ 安全做法:使用 AbortController 或检查状态
// 更推荐使用 Composition API 封装
}
}2. 使用 Composition API 封装
js
// utils.js
export function useInterval(callback, delay) {
const timer = ref(null);
onMounted(() => {
if (delay !== null) {
timer.value = setInterval(callback, delay);
}
});
onBeforeUnmount(() => {
if (timer.value) clearInterval(timer.value);
});
return timer;
}六、总结
组件实例的生命周期是一个精心编排的过程,Vue 通过内部状态(如 _isMounted、_isDestroyed)精确地控制其各个阶段:
_isMounted:确保 DOM 操作只在挂载后执行。_isDestroyed:防止内存泄漏,确保资源被正确清理。_isDeactivated:支持<keep-alive>的缓存机制。
这些内部状态是 Vue 实现高效、可靠渲染的核心。作为开发者,你应通过生命周期钩子来介入这一过程,并遵循最佳实践,确保应用的健壮性和性能。