前端核心知识点 04
组件实例的生命周期
组件生命周期的宏观流程
一个组件实例的生命周期可以分为三个主要阶段:
- 创建(Creation):实例化、设置响应式、编译模板。
- 挂载与更新(Mounting & Update):插入 DOM、响应数据变化。
- 卸载(Unmounting):从 DOM 移除、清理资源。
生命周期钩子函数
Vue 提供了一系列生命周期钩子,允许你在特定阶段执行代码。
创建阶段
setup():
- 组合式 API 的入口。
- 在 beforeCreate 和 created 之间执行。
- 返回 setup 中定义的响应式状态和方法。
beforeCreate(选项式):
- 实例初始化后,数据观测 (data observation) 和事件/watcher 配置之前被调用。
created:
实例创建完成。 数据、计算属性、方法、watch/event 回调都已设置。 可以访问响应式数据。 但尚未挂载到 DOM,$el 为 undefined。
挂载阶段
beforeMount:
- 挂载开始前被调用。
- render 函数首次被调用。
- 虚拟 DOM 已创建,但尚未生成真实 DOM。
mounted:
- 组件已挂载到 DOM。
- el 属性现在可用。
- 可以安全地访问和操作 DOM。
- 适合发起网络请求、设置定时器、集成第三方库。
更新阶段
beforeUpdate:
- 响应式数据变化,导致重新渲染之前调用。
- 虚拟 DOM 即将重新渲染。
- DOM 仍是旧的状态。
updated:
- 组件因响应式数据变化而重新渲染后调用。
- DOM 已更新。
- 注意:避免在此钩子中修改状态,可能引发无限循环。
卸载阶段
beforeUnmount(原 beforeDestroy):
- 实例卸载之前调用。
- 组件实例仍然完全正常。
- 清理定时器、取消订阅、移除事件监听器。
unmounted(原 destroyed):
- 组件实例已从 DOM 中移除。
- 所有指令都被解绑,所有事件监听器被移除,所有子组件实例被卸载。
- 实例不可用。
KeepAlive 缓存机制
KeepAlive 是 Vue 3 中一个极其重要的内置组件,它通过缓存组件实例来避免重复的销毁与重建,从而保留组件状态、提升性能。 理解其缓存机制、配置选项和生命周期钩子,是构建高性能 Vue 应用的关键。
核心作用:保存组件状态
当一个组件被 KeepAlive 包裹时
- 首次渲染:正常创建、挂载。
- 切换离开:不销毁,而是被缓存起来(失活)。
- 再次进入:不重新创建,而是从缓存中取出并激活。
保留了什么?
组件实例:data、ref、reactive 状态。 DOM 状态:表单输入、滚动位置、动画状态。 子组件树:整个组件子树都被缓存。
配置选项的实现
KeepAlive 支持三个关键属性:include、exclude、max。
include 和 exclude
- 作用:根据组件的 name 决定是否缓存。
- 类型:字符串、正则、数组。
- 优先级:exclude 优先于 include。
<KeepAlive
:include="/^My.*/"
:exclude="['Modal']"
>
<component :is="currentView" />
</KeepAlive>max
- 作用:限制缓存的最大数量,防止内存泄漏。
- 实现:LRU 策略。
- LRU 更新:每次组件被激活(onActivated),其 key 会从 keys 中移除并重新添加到末尾,确保“最近使用”的排在后面。
生命周期钩子:onActivated 与 onDeactivated
由于 KeepAlive 组件不会被销毁,mounted/unmounted 钩子只在首次和最终销毁时触发。为此,Vue 提供了两个专用钩子。
onActivated()
触发时机:
- 组件从缓存中被取出并重新插入 DOM 时。
- 首次 mounted 之后也会触发一次。
用途:
- 启动定时器、动画。
- 恢复事件监听。
- 触发数据更新(如轮询)。
import { onActivated, onDeactivated } from 'vue';
onActivated(() => {
console.log('组件被激活');
startTimer();
});onDeactivated()
触发时机:
- 组件被切换离开,放入缓存时。
用途:
- 清理定时器、取消订阅,避免内存泄漏。
- 暂停动画、音乐。
onDeactivated(() => {
console.log('组件被缓存(失活)');
clearInterval(timer);
// 不要调用 unmount,KeepAlive 会处理
});钩子执行顺序
// 首次进入
created → mounted → onActivated
// 切换离开
onDeactivated
// 再次进入
onActivated
// 最终销毁(如被 exclude 或 max 淘汰)
onDeactivated → beforeUnmount → unmountedvue 插槽机制
插槽的基本概念与使用
插槽是组件接口的一部分,允许父组件向子组件插入内容。最基本的用法是默认插槽:
<!-- 子组件 BaseLayout.vue -->
<template>
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot> <!-- 默认插槽 -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template><!-- 父组件使用 -->
<template>
<BaseLayout>
<template #header>
<h1>这里是标题</h1>
</template>
<p>这是主要内容</p>
<template #footer>
<p>版权所有</p>
</template>
</BaseLayout>
</template>动态插槽名与插槽的高级用法
<template>
<MyComponent>
<template #[dynamicSlotName]="slotProps">
{{ slotProps.text }}
</template>
</MyComponent>
</template>编译器会将动态插槽名编译为动态属性:
function render() {
return h(MyComponent, null, {
[this.dynamicSlotName]: (slotProps) => h('span', slotProps.text)
});
}插槽与组件生命周期
插槽内容的渲染时机与组件的生命周期密切相关。插槽函数在子组件渲染时执行,这意味着:
- 插槽可以访问子组件的当前状态
- 插槽内容的更新由子组件的响应式系统驱动
- 插槽函数每次渲染时都会重新执行
// 子组件中插槽的渲染过程
function render() {
return h('div', [
// 渲染默认插槽
renderSlot(this.$slots, 'default', {
// 传递给作用域插槽的数据
data: this.localData
}),
// 渲染具名插槽
renderSlot(this.$slots, 'header')
]);
}性能优化与注意事项
Vue 在插槽处理上做了多项优化:
- 插槽函数缓存:对于非动态插槽,Vue 会缓存插槽函数以避免重复创建
- 编译时优化:编译器会标记插槽的类型,运行时根据标记采用不同的处理策略
- 响应式追踪:插槽函数内部的响应式依赖会被正确追踪
需要注意的几点:
- 插槽内容在父组件的作用域中定义,但在子组件的上下文中渲染
- 作用域插槽通过函数参数传递数据,实现了父子组件间的安全数据流
- 插槽函数每次执行都会创建新的 VNode,需要注意性能影响
effectScope
effectScope 的基本概念与使用
在 Vue 3 的响应式系统中,effectScope 是一个相对高级但非常实用的 API,它允许开发者更精细地控制响应式副作用(effects)的生命周期。 通过 effectScope,我们可以将一组相关的副作用组织在一起,并能够一次性停止它们,这对于构建复杂的响应式系统和插件非常重要。
import { effectScope, ref, watchEffect } from 'vue'
// 创建一个 effect 作用域
const scope = effectScope()
// 在作用域内运行副作用
scope.run(() => {
const count = ref(0)
watchEffect(() => {
console.log(`Count: ${count.value}`)
})
setInterval(() => {
count.value++
}, 1000)
})
// 在适当的时候停止所有副作用
scope.stop()effectScope 的工作机制
effectScope 的实现基于 Vue 的响应式系统内部机制。每个 effectScope 实例本质上是一个副作用收集器,它会跟踪在其内部创建的所有副作用
effectScope 与组件生命周期
在 Vue 组件中,每个组件实例都会创建自己的 effectScope,用于收集该组件的所有副作用 当组件被卸载时,其对应的 effectScope 会被自动停止,从而清理所有副作用。 这种方式确保了组件的副作用能够随着组件的生命周期自动管理,避免了内存泄漏。
独立作用域与嵌套作用域
effectScope 支持创建独立作用域和嵌套作用域
effectScope 在实际应用中的使用场景
- 插件开发
- 可组合函数(Composables)
- 复杂的状态管理
总结
effectScope 是 Vue 3 响应式系统中一个强大的机制,它提供了一种精细化管理副作用生命周期的方式:
- 作用域管理:通过 effectScope 可以将相关的副作用组织在一起
- 生命周期控制:通过 scope.stop() 可以一次性停止作用域内的所有副作用
- 内存管理:合理的使用 effectScope 可以避免内存泄漏
- 插件友好:为插件和可组合函数提供了更好的副作用管理方式
effectScope 的引入完善了 Vue 的响应式系统,使得开发者能够更精确地控制副作用的创建和销毁,特别是在构建复杂应用、插件或可组合函数时, 它是一个非常有价值的工具。
v-memo 的基本概念与使用
v-memo 是 Vue 3.2 引入的一个编译期优化指令,它接受一个数组作为参数,数组中的每一项都代表一个依赖值。 当这些依赖值都没有发生变化时,Vue 会跳过该元素及其所有子元素的更新。
<template>
<div v-memo="[valueA, valueB]">
<ExpensiveComponent :data="complexData" />
<div>
<p>{{ valueA }}</p>
<p>{{ valueB }}</p>
</div>
</div>
</template>在上面的例子中,只有当 valueA 或 valueB 发生变化时,整个 <div> 及其子元素才会重新渲染。 如果这两个值都没有变化,即使 complexData 发生了变化,也会跳过更新。
v-memo 与 v-for 的结合使用
v-memo 最强大的场景之一是与 v-for 结合使用,用于优化大型列表的渲染:
<template>
<div>
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id, item.value]"
>
<ExpensiveListItem :item="item" />
<div class="item-details">
<p>ID: {{ item.id }}</p>
<p>Value: {{ item.value }}</p>
<p>Last updated: {{ item.lastUpdated }}</p>
</div>
</div>
</div>
</template>在这个例子中,对于列表中的每一项,只有当 item.id 或 item.value 发生变化时,该项才会重新渲染。 即使 item.lastUpdated 或其他属性发生变化,也会跳过更新。
编译后,Vue 会为每个列表项生成独立的 memo 缓存:
// 编译后的简化代码
function render() {
return renderList(this.list, (item, index) => {
return withMemo(
[item.id, item.value],
() => {
return h('div', { key: item.id }, [
h(ExpensiveListItem, { item }),
h('div', { className: 'item-details' }, [
h('p', `ID: ${item.id}`),
h('p', `Value: ${item.value}`),
h('p', `Last updated: ${item.lastUpdated}`)
])
])
},
this,
index // 每个列表项有唯一的索引
)
})
}v-memo 的运行时处理
在 Vue 的运行时中,v-memo 的处理涉及到组件更新过程中的优化判断:
// 简化的 patch 过程
function patch(oldVNode, newVNode, container) {
// 检查是否是 memo 组件
if (newVNode.memo) {
// 检查依赖是否发生变化
if (!isMemoDirty(oldVNode.memoData, newVNode.memo)) {
// 依赖未变化,直接复用旧的 vnode
newVNode.el = oldVNode.el
newVNode.component = oldVNode.component
return
}
}
// 正常的 patch 流程
// ...
}当 Vue 检测到一个带有 v-memo 的节点时,会首先检查其依赖是否发生变化。如果没有变化,就会跳过整个子树的更新过程,直接复用之前的 DOM 节点。
v-memo 的高级使用场景
- 优化复杂组件树
- 与计算属性结合使用
- 在动态组件中的使用
性能考量与注意事项
虽然 v-memo 是一个强大的优化工具,但使用时需要注意以下几点:
依赖数组的正确性:依赖数组必须包含所有可能影响子树渲染的响应式数据。遗漏依赖会导致界面不更新,而包含过多依赖会降低优化效果。
内存开销:每个使用 v-memo 的元素都会创建缓存,对于大型列表可能会增加内存使用。
适用场景:v-memo 主要适用于以下场景:
- 昂贵的组件树渲染
- 大型列表的优化
- 频繁更新但只有部分数据变化的场景