Vue 生命周期实现原理
深入剖析 Vue2 与 Vue3 生命周期钩子的注册机制、调用时机及底层源码实现,理解生命周期与组件渲染的深层关系。
一、前言
生命周期是 Vue 框架最核心的概念之一,它定义了组件从创建到销毁的完整过程。理解生命周期的实现原理,不仅能帮助我们在正确的时机执行逻辑,更能深入理解 Vue 的渲染机制、响应式系统与组件更新策略。
Vue2 与 Vue3 在生命周期设计上既有传承也有变革:Vue3 引入了 Composition API,setup 函数成为新的逻辑组织中心,生命周期钩子也随之发生了重要变化。本文将从源码层面剖析两者的实现差异,揭示生命周期背后的设计哲学。
二、核心内容
2.1 Vue2 生命周期钩子注册机制
Vue2 的生命周期钩子通过选项式 API 在组件定义时声明。在初始化阶段,Vue 会将这些钩子函数收集到实例的 $options 对象中。
// Vue2 生命周期选项定义
const LIFECYCLE_HOOKS = [
'beforeCreate',
'created',
'beforeMount',
'mounted',
'beforeUpdate',
'updated',
'beforeDestroy',
'destroyed',
'activated',
'deactivated',
'errorCaptured',
'serverPrefetch'
];
// 初始化时合并策略
function mergeHook(parentVal, childVal) {
return childVal
? parentVal
? parentVal.concat(childVal)
: Array.isArray(childVal)
? childVal
: [childVal]
: parentVal;
}
LIFECYCLE_HOOKS.forEach(hook => {
strats[hook] = mergeHook;
});
在 initLifecycle 阶段,Vue 会遍历 $options 中的生命周期配置,将其转换为内部调用队列。每个生命周期钩子实际上是一个函数数组,支持通过 mixin 多次注册同一钩子。
2.2 Vue3 生命周期钩子的变化
Vue3 对生命周期进行了重大调整,主要体现在两个方面:
命名变更:
beforeDestroy→beforeUnmountdestroyed→unmounted
Composition API 引入:
import { onMounted, onUpdated, onUnmounted } from 'vue'
export default {
setup() {
// 在 setup 中注册生命周期钩子
onMounted(() => {
console.log('组件已挂载')
})
onUpdated(() => {
console.log('组件已更新')
})
onUnmounted(() => {
console.log('组件已卸载')
})
}
}
Vue3 的生命周期钩子需要在 setup 函数的同步执行期间调用,它们通过全局上下文关联到当前正在初始化的组件实例。
2.3 钩子调用时机源码分析
Vue2 生命周期调用流程
Vue2 的生命周期调用贯穿在 _init、$mount 和 _update 方法中:
// core/instance/init.js
Vue.prototype._init = function(options) {
const vm = this;
// 合并选项
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
vm
);
// 初始化生命周期
initLifecycle(vm);
initEvents(vm);
initRender(vm);
// 调用 beforeCreate
callHook(vm, 'beforeCreate');
// 初始化注入、响应式数据
initInjections(vm);
initState(vm); // props、methods、data、computed、watch
initProvide(vm);
// 调用 created
callHook(vm, 'created');
// 执行挂载
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
// 通用的钩子调用函数
function callHook(vm, hook) {
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0, j = handlers.length; i < j; i++) {
try {
handlers[i].call(vm);
} catch (e) {
handleError(e, vm, `${hook} hook`);
}
}
}
}
Vue3 生命周期调用流程
Vue3 将生命周期调用分散在 setupComponent 和 render 流程中:
// runtime-core/component.ts
function setupComponent(instance) {
const { props, children } = instance.vnode;
const isStateful = isStatefulComponent(instance);
initProps(instance, props, isStateful, isStateful);
initSlots(instance, children);
const setupResult = isStateful
? setupStatefulComponent(instance)
: undefined;
return setupResult;
}
function setupStatefulComponent(instance) {
const Component = instance.type;
// 创建 setup 上下文
instance.accessCache = Object.create(null);
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
const { setup } = Component;
if (setup) {
const setupContext = createSetupContext(instance);
// 设置当前实例,供生命周期钩子注册使用
setCurrentInstance(instance);
const setupResult = callWithErrorHandling(
setup,
instance,
ErrorCodes.SETUP_FUNCTION,
[shallowReadonly(instance.props), setupContext]
);
unsetCurrentInstance();
// 处理 setup 返回值
handleSetupResult(instance, setupResult);
}
}
2.4 生命周期与渲染的关系
生命周期与渲染流程紧密耦合。以 mounted 为例,它标志着虚拟 DOM 已经转换为真实 DOM 并插入文档:
// Vue2 挂载流程中的 mounted 调用
Vue.prototype._update = function(vnode, hydrating) {
const vm = this;
const prevEl = vm.$el;
const prevVnode = vm._vnode;
if (!prevVnode) {
// 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false);
// 新根节点,调用 mounted
if (vm.$parent && vm.$parent._vnode) {
vm.$parent._vnode.children = vm.$vnode;
}
callHook(vm, 'mounted');
} else {
// 更新渲染
vm.$el = vm.__patch__(prevVnode, vnode);
callHook(vm, 'updated');
}
};
Vue3 中通过 queuePostRenderEffect 确保生命周期回调在渲染完成后执行:
// runtime-core/renderer.ts
const mountComponent = (initialVNode, container) => {
const instance = initialVNode.component = createComponentInstance(initialVNode);
setupComponent(instance);
setupRenderEffect(instance, initialVNode, container);
};
const setupRenderEffect = (instance, initialVNode, container) => {
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 首次渲染
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container);
initialVNode.el = subTree.el;
instance.isMounted = true;
// 调用 mounted 钩子
queuePostRenderEffect(() => {
instance.a && invokeArrayFns(instance.a); // mounted hooks
});
} else {
// 更新渲染
const nextTree = renderComponentRoot(instance);
const prevTree = instance.subTree;
instance.subTree = nextTree;
patch(prevTree, nextTree);
// 调用 updated 钩子
queuePostRenderEffect(() => {
instance.u && invokeArrayFns(instance.u); // updated hooks
});
}
};
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(updateComponent),
instance.scope
));
const updateComponent = (instance.update = () => effect.run());
updateComponent();
};
2.5 setup 中的生命周期实现
Vue3 的 setup 中生命周期钩子通过全局状态管理当前实例:
// runtime-core/component.ts
let currentInstance = null;
export const getCurrentInstance = () => currentInstance;
export const setCurrentInstance = (instance) => {
currentInstance = instance;
};
// onMounted 实现
export const onMounted = (hook, target = currentInstance) => {
if (target) {
// 将钩子推入 mounted 数组
(target.m || (target.m = [])).push(hook);
}
};
// onUnmounted 实现
export const onUnmounted = (hook, target = currentInstance) => {
if (target) {
(target.um || (target.um = [])).push(hook);
}
};
这种设计允许在 setup 的任意嵌套函数中注册生命周期钩子,只要调用时存在当前实例上下文即可。
2.6 错误处理钩子
Vue 提供了 errorCaptured(Vue2)和 onErrorCaptured(Vue3)用于捕获组件树中的错误:
// Vue2 errorCaptured
export default {
errorCaptured(err, vm, info) {
console.error('捕获到错误:', err);
console.error('出错的组件:', vm);
console.error('错误信息:', info);
// 返回 false 阻止错误继续向上传播
return false;
}
}
// Vue3 onErrorCaptured
import { onErrorCaptured } from 'vue'
export default {
setup() {
onErrorCaptured((err, instance, info) => {
console.error('捕获到错误:', err);
// 返回 false 阻止传播
return false;
});
}
}
错误处理钩子的调用遵循组件树向上冒泡的机制,直到被捕获或到达根组件。
三、Mermaid 图表
Vue2 生命周期完整流程
Vue2 vs Vue3 生命周期对比
四、代码示例
示例 1:Vue2 生命周期完整演示
// lifecycle-demo.vue
export default {
name: 'LifecycleDemo',
data() {
return {
message: 'Hello Vue2',
timer: null
};
},
beforeCreate() {
console.log('beforeCreate: 实例初始化完成,数据观测和事件未设置');
// 此时无法访问 data、computed、methods
// console.log(this.message); // undefined
},
created() {
console.log('created: 数据观测完成,可以访问数据');
console.log(this.message); // 'Hello Vue2'
// 适合进行异步数据获取
this.fetchData();
},
beforeMount() {
console.log('beforeMount: 模板编译完成,即将挂载到 DOM');
console.log(this.$el); // undefined
},
mounted() {
console.log('mounted: 已挂载到 DOM');
console.log(this.$el); // DOM 元素
// 启动定时器
this.timer = setInterval(() => {
this.message = new Date().toLocaleTimeString();
}, 1000);
},
beforeUpdate() {
console.log('beforeUpdate: 数据已变化,即将重新渲染');
// 可以获取更新前的 DOM 状态
},
updated() {
console.log('updated: 重新渲染完成');
// 避免在此处修改数据,可能导致无限循环
},
beforeDestroy() {
console.log('beforeDestroy: 实例即将销毁');
// 清理工作
if (this.timer) {
clearInterval(this.timer);
this.timer = null;
}
},
destroyed() {
console.log('destroyed: 实例已销毁');
},
methods: {
fetchData() {
// 模拟异步请求
setTimeout(() => {
this.message = '数据加载完成';
}, 500);
}
}
};
示例 2:Vue3 Composition API 生命周期
<!-- lifecycle-composition.vue -->
<template>
<div>
<h1>{{ count }}</h1>
<button @click="increment">增加</button>
</div>
</template>
<script>
import {
ref,
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
onErrorCaptured
} from 'vue';
export default {
name: 'LifecycleComposition',
setup() {
const count = ref(0);
let timer = null;
// 等效于 beforeCreate + created
console.log('setup: 在组件创建时执行');
onBeforeMount(() => {
console.log('onBeforeMount: 挂载前');
});
onMounted(() => {
console.log('onMounted: 已挂载');
// 启动定时器
timer = setInterval(() => {
console.log('定时器运行中...');
}, 2000);
});
onBeforeUpdate(() => {
console.log('onBeforeUpdate: 更新前');
});
onUpdated(() => {
console.log('onUpdated: 更新完成');
});
onBeforeUnmount(() => {
console.log('onBeforeUnmount: 卸载前');
// 清理副作用
if (timer) {
clearInterval(timer);
timer = null;
}
});
onUnmounted(() => {
console.log('onUnmounted: 已卸载');
});
onErrorCaptured((err, instance, info) => {
console.error('捕获错误:', err.message);
console.error('错误来源:', info);
return false; // 阻止传播
});
const increment = () => {
count.value++;
};
return {
count,
increment
};
}
};
</script>
示例 3:自定义生命周期组合式函数
// composables/useLifecycle.js
import { onMounted, onUnmounted } from 'vue';
/**
* 自动清理的定时器
*/
export function useInterval(callback, delay) {
let timer = null;
onMounted(() => {
if (delay !== null) {
timer = setInterval(callback, delay);
}
});
onUnmounted(() => {
if (timer) {
clearInterval(timer);
timer = null;
}
});
return {
clear: () => {
if (timer) {
clearInterval(timer);
timer = null;
}
}
};
}
/**
* 监听窗口大小变化
*/
export function useWindowResize(callback) {
const handler = () => {
callback({
width: window.innerWidth,
height: window.innerHeight
});
};
onMounted(() => {
window.addEventListener('resize', handler);
handler(); // 立即执行一次
});
onUnmounted(() => {
window.removeEventListener('resize', handler);
});
}
// 使用示例
import { ref } from 'vue';
import { useInterval, useWindowResize } from './composables/useLifecycle.js';
export default {
setup() {
const count = ref(0);
const windowSize = ref({ width: 0, height: 0 });
// 使用组合式函数
useInterval(() => {
count.value++;
}, 1000);
useWindowResize((size) => {
windowSize.value = size;
});
return {
count,
windowSize
};
}
};
五、常见问题
Q1:为什么 beforeCreate 中无法访问 data?
因为在 beforeCreate 阶段,Vue 尚未执行 initState,数据观测(data observation)和事件系统还未初始化。此时实例只有基本的属性和事件配置,无法访问响应式数据、计算属性和方法。
Q2:created 和 mounted 的区别是什么?
created:实例已创建,数据观测完成,但 DOM 尚未生成,无法访问$elmounted:虚拟 DOM 已渲染为真实 DOM 并插入文档,可以访问$el
如果需要在 DOM 操作后执行逻辑(如初始化第三方图表库),必须在 mounted 中进行。
Q3:Vue3 中 setup 替代了哪些生命周期?
setup 函数在 beforeCreate 和 created 之间执行,本质上替代了这两个钩子。在 setup 中可以直接访问 props 和 setup 内部定义的响应式数据,但无法访问 data、computed 等选项式 API 定义的内容。
Q4:为什么 updated 中修改数据会导致无限循环?
updated 钩子会在组件重新渲染后调用。如果在此钩子中修改响应式数据,会触发新的更新,再次进入 updated,形成无限循环。Vue 虽然有一些防护措施,但仍应避免这种写法。
Q5:onMounted 在异步 setup 中如何工作?
onMounted 必须在 setup 的同步执行阶段调用。如果 setup 返回 Promise(异步 setup),钩子注册需要在 await 之前完成:
setup() {
onMounted(() => {
console.log('这可以正常工作');
});
await fetchData(); // 异步操作
// 以下代码在 await 之后,但 onMounted 已经注册成功
}
六、总结
生命周期是连接开发者与 Vue 内部机制的桥梁。通过本文的源码分析,我们可以得出以下关键结论:
- 注册机制:Vue2 通过选项合并将钩子收集到
$options,Vue3 通过全局上下文在setup中动态注册 - 调用时机:生命周期钩子嵌入在渲染流程的关键节点,与虚拟 DOM 的 patch 过程紧密耦合
- 设计演进:Vue3 的 Composition API 使生命周期使用更加灵活,支持更好的逻辑复用
- 清理义务:在
beforeDestroy/beforeUnmount中清理副作用(定时器、事件监听、订阅)是最佳实践
理解生命周期的底层实现,有助于我们在复杂场景下做出正确的技术决策,写出更健壮的 Vue 应用。
七、思考题
-
在 Vue3 中,如果在一个嵌套的
setTimeout回调中调用onMounted,会发生什么?为什么? -
设计一个自定义组合式函数
useLifecycleLogger,能够记录组件每个生命周期的执行时间并输出日志。 -
分析 Vue3 中
keep-alive组件对生命周期的影响,为什么被缓存的组件会触发activated和deactivated而不是mounted和unmounted? -
在服务端渲染(SSR)场景下,哪些生命周期钩子不会执行?为什么
serverPrefetch被引入?

2198

被折叠的 条评论
为什么被折叠?



