各位观众老爷,大家好!今天咱们来聊聊Vue组件生命周期那些事儿。这玩意儿听起来玄乎,其实就是Vue组件从出生到死亡的各个阶段,以及在这些阶段Vue允许咱们插手做点事情的机会。
咱们主要扒一扒onMounted
, onUpdated
, 和 onUnmounted
这三个“生命大事件”的源码,看看Vue是怎么把它们安排得明明白白的。
一、Vue组件生命周期概述: 组件的一生
首先,得简单回顾下Vue组件的生命周期。你可以把它想象成一个人的生命历程:
- 出生(创建):组件被创建,进行初始化,设置数据等等。对应
beforeCreate
和created
钩子。 - 挂载:组件被渲染到DOM树上,真正显示在页面中。对应
beforeMount
和mounted
钩子。 - 更新:组件的数据发生变化,导致重新渲染。对应
beforeUpdate
和updated
钩子。 - 卸载:组件从DOM树上移除,不再显示。对应
beforeUnmount
和unmounted
钩子。
当然,还有一些其他的钩子,比如activated
和deactivated
,它们主要用在<keep-alive>
组件中,咱们今天就不深入研究了。
二、onMounted
:组件的“满月酒”
onMounted
钩子会在组件挂载完毕后执行。这意味着什么?意味着组件已经被渲染到DOM中,你可以访问到真实的DOM元素,并且可以进行一些需要操作DOM的操作,比如初始化第三方库、绑定事件监听器等等。
源码探秘:onMounted
的调用时机
Vue3的挂载过程非常复杂,涉及到虚拟DOM、渲染器等等。咱们简化一下,只关注onMounted
钩子的调用。
挂载的核心函数是mountComponent
。在这个函数中,会调用setupRenderEffect
来创建渲染副作用。在渲染副作用函数中,会执行组件的渲染函数,生成虚拟DOM。然后,通过patch
函数将虚拟DOM转换为真实DOM,并挂载到页面上。
在patch
函数的最后,如果是一个新的组件实例,会调用invokeMountHook
函数:
// 简化版invokeMountHook
function invokeMountHook(vnode, initialVNode, isHydration = false) {
const instance = vnode.component;
if (instance) {
const { m } = instance.hooks; // instance.hooks 存储了组件的生命周期钩子函数
if (m) {
for (let i = 0; i < m.length; i++) {
m[i].call(instance.proxy); // 调用onMounted钩子
}
}
}
}
可以看到,invokeMountHook
函数从组件实例的hooks
属性中取出m
数组,这个m
数组就是存储onMounted
钩子函数的地方。然后,遍历这个数组,逐个调用钩子函数。
代码示例:
<template>
<div>
<p ref="myParagraph">Hello, World!</p>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const myParagraph = ref(null);
onMounted(() => {
console.log('组件挂载完毕!');
console.log('DOM元素:', myParagraph.value); // 可以访问到DOM元素
myParagraph.value.textContent = '组件挂载后修改了内容!';
});
return {
myParagraph
};
}
};
</script>
在这个例子中,onMounted
钩子会在组件挂载完毕后执行,此时myParagraph.value
指向真实的<p>
元素,我们可以修改它的内容。
总结:
onMounted
钩子在组件挂载到DOM后执行。- 可以访问到真实的DOM元素。
- 适合进行DOM操作、初始化第三方库等操作。
三、onUpdated
:组件的“回炉重造”
onUpdated
钩子会在组件因为数据变化导致重新渲染后执行。这意味着什么?意味着组件的DOM已经更新,你可以根据新的数据状态进行一些调整。
源码探秘:onUpdated
的调用时机
onUpdated
的调用位置和onMounted
类似,也是在patch
函数中。但是,onUpdated
是在patch
函数完成对组件的更新后才调用的。
// 简化版patch
function patch(...) {
// ... 省略虚拟DOM diff 和 更新逻辑 ...
if (vnode.component) {
// ... 更新组件 ...
if (instance.update) { // 关键:执行组件的更新函数
instance.update();
}
}
// 组件更新完成后调用 onUpdated
if (initialVNode === null) { // 只有初始挂载时 initialVNode 为 null,更新时则有值
invokePostUpdateHooks(vnode);
}
}
function invokePostUpdateHooks(vnode) {
const instance = vnode.component;
if (instance) {
const { u } = instance.hooks; // instance.hooks 存储了组件的生命周期钩子函数
if (u) {
for (let i = 0; i < u.length; i++) {
u[i].call(instance.proxy); // 调用onUpdated钩子
}
}
}
}
可以看到,在patch
函数中,如果当前vnode是一个组件,并且组件已经更新完毕,会调用invokePostUpdateHooks
函数。这个函数从组件实例的hooks
属性中取出u
数组,这个u
数组就是存储onUpdated
钩子函数的地方。然后,遍历这个数组,逐个调用钩子函数。
代码示例:
<template>
<div>
<p ref="myParagraph">{{ message }}</p>
<button @click="updateMessage">更新消息</button>
</div>
</template>
<script>
import { ref, onUpdated } from 'vue';
export default {
setup() {
const message = ref('Hello, World!');
const myParagraph = ref(null);
const updateMessage = () => {
message.value = 'Hello, Vue!';
};
onUpdated(() => {
console.log('组件更新完毕!');
console.log('新的消息:', message.value);
console.log('DOM元素:', myParagraph.value); // 可以访问到更新后的DOM元素
});
return {
message,
myParagraph,
updateMessage
};
}
};
</script>
在这个例子中,每次点击按钮更新message
的值时,组件都会重新渲染,并且onUpdated
钩子会被执行。
注意:
onUpdated
钩子会在每次组件更新后执行,所以要避免在onUpdated
中直接修改数据,否则可能导致无限循环更新。- 可以使用
nextTick
来确保在DOM更新完成后再执行一些操作。
总结:
onUpdated
钩子在组件更新后执行。- 可以访问到更新后的DOM元素。
- 避免在
onUpdated
中直接修改数据。 - 可以使用
nextTick
来确保在DOM更新完成后再执行操作。
四、onUnmounted
:组件的“告别仪式”
onUnmounted
钩子会在组件被卸载之前执行。这意味着什么?意味着组件即将从DOM树上移除,你可以进行一些清理工作,比如取消事件监听器、清除定时器等等。
源码探秘:onUnmounted
的调用时机
卸载的过程发生在unmount
函数中。这个函数会递归地卸载组件及其子组件。
// 简化版unmount
function unmount(vnode, parentComponent, parentSuspense, doRemove = false, optimized = false) {
// ... 省略其他卸载逻辑 ...
const { component, shapeFlag } = vnode;
if (component) {
// 调用组件的 beforeUnmount 钩子
if (component.isDeactivated) {
return;
}
const { bum } = component.hooks; // 这里的 bum 对应的是 beforeUnmount 钩子
if (bum) {
for (let i = 0; i < bum.length; i++) {
bum[i].call(component.proxy);
}
}
// 调用组件的 unmounted 钩子
const { um } = component.hooks; // 这里的 um 对应的是 unmounted 钩子
if (um) {
for (let i = 0; i < um.length; i++) {
um[i].call(component.proxy); // 调用onUnmounted钩子
}
}
}
// ... 省略移除DOM元素的逻辑 ...
}
可以看到,在unmount
函数中,会先调用组件的beforeUnmount
钩子(如果存在),然后再调用onUnmounted
钩子。um
数组就是存储onUnmounted
钩子函数的地方。然后,遍历这个数组,逐个调用钩子函数。
代码示例:
<template>
<div>
<p ref="myParagraph">Hello, World!</p>
</div>
</template>
<script>
import { ref, onMounted, onUnmounted } from 'vue';
export default {
setup() {
const myParagraph = ref(null);
let timer = null;
onMounted(() => {
timer = setInterval(() => {
console.log('定时器正在运行...');
}, 1000);
});
onUnmounted(() => {
console.log('组件卸载!');
clearInterval(timer); // 清除定时器
console.log('定时器已清除!');
});
return {
myParagraph
};
}
};
</script>
在这个例子中,onMounted
钩子会启动一个定时器,onUnmounted
钩子会在组件卸载时清除这个定时器,防止内存泄漏。
总结:
onUnmounted
钩子在组件卸载前执行。- 适合进行清理工作,比如取消事件监听器、清除定时器等。
- 防止内存泄漏。
五、生命周期钩子函数的执行顺序
为了更清楚地理解生命周期钩子函数的执行顺序,咱们用一个表格来总结一下:
阶段 | 钩子函数 | 说明 |
---|---|---|
创建阶段 | beforeCreate |
组件实例被创建之前,data 和 methods 尚未初始化。 |
created |
组件实例创建完成,data 和 methods 已经初始化,但尚未挂载到DOM。 | |
挂载阶段 | beforeMount |
组件挂载到DOM之前。 |
mounted |
组件挂载到DOM之后,可以访问到真实的DOM元素。 | |
更新阶段 | beforeUpdate |
组件更新之前。 |
updated |
组件更新之后,可以访问到更新后的DOM元素。 | |
卸载阶段 | beforeUnmount |
组件卸载之前。 |
unmounted |
组件卸载之后。 |
六、注意事项和最佳实践
- 避免在
created
钩子中进行DOM操作:因为此时组件尚未挂载到DOM,无法访问到真实的DOM元素。 - 避免在
onUpdated
钩子中直接修改数据:可能导致无限循环更新。 - 在
onUnmounted
钩子中进行清理工作:防止内存泄漏。 - 合理利用
nextTick
:确保在DOM更新完成后再执行一些操作。 - 理解父子组件的生命周期执行顺序:父组件的
mounted
钩子会在子组件的mounted
钩子之后执行,父组件的updated
钩子会在子组件的updated
钩子之后执行,父组件的unmounted
钩子会在子组件的unmounted
钩子之前执行。
七、总结
Vue组件的生命周期钩子函数是Vue提供给我们的“插手”组件行为的机会。理解它们的执行时机和顺序,能够帮助我们更好地控制组件的行为,编写更高效、更健壮的Vue应用。
希望今天的讲解对大家有所帮助。记住,理解源码是提升技能的最好方法之一。下次遇到问题,不妨打开源码,看看Vue是怎么实现的,你会发现更多有趣的东西。
好了,今天的讲座就到这里,下次再见!