10|动态组件、缓存与异步组件
第二章:组件模型与通信纵览 / 10。
<component :is>选择渲染谁,<KeepAlive>决定离开时是否销毁,defineAsyncComponent决定组件代码何时加载;三者不要混成一个“动态加载”。
先只做动态选择
<script setup lang="ts">
import { ref } from 'vue'
const draft = ref('')
</script>
<template>
<label>昵称 <input v-model="draft" /></label>
</template><template>
<p>安全设置</p>
</template><script setup lang="ts">
import { computed, ref } from 'vue'
import ProfilePanel from './ProfilePanel.vue'
import SecurityPanel from './SecurityPanel.vue'
const panels = {
profile: ProfilePanel,
security: SecurityPanel
} as const
type PanelName = keyof typeof panels
const active = ref<PanelName>('profile')
const currentPanel = computed(() => panels[active.value])
</script>
<template>
<button type="button" @click="active = 'profile'">资料</button>
<button type="button" @click="active = 'security'">安全</button>
<component :is="currentPanel" />
</template>currentPanel 的值是导入的组件对象。比起把 'ProfilePanel' 字符串交给运行时再次查找,这种注册表能让 TypeScript 限制合法 key,也保留组件的静态依赖关系。
切换时实例发生了什么
不加缓存时:
profile 首次出现 → 挂载 ProfilePanel,draft=''
输入 Ada → draft='Ada'
切到 security → 卸载 ProfilePanel,实例与 draft 消失
切回 profile → 创建新实例,draft 又是 ''若产品要求保留未提交草稿,把动态组件放进 <KeepAlive>:
<KeepAlive :max="2">
<component :is="currentPanel" />
</KeepAlive>此时离开的实例被停用而不是立即卸载,回来时重新激活。需要暂停计时器或刷新数据的组件,应分别使用 onDeactivated 与 onActivated;不要误以为缓存组件每次回来都会重新 onMounted。
缓存有内存成本。表单本来就应重置、数据必须每次刷新或内容敏感时,不使用 KeepAlive 反而是正确选择。include / exclude 依赖组件 name,SFC 在本课程基线下通常可由文件名推导;重命名文件时要一起检查缓存规则。
在响应式状态中保存组件对象
正文用普通常量注册表,因此组件定义不会被深度代理。若必须直接把当前组件放进响应式引用,使用 shallowRef<Component>;若组件对象嵌在 reactive 集合中,用 markRaw。把组件定义放进普通 ref / reactive 会产生不必要代理和警告。
异步组件把代码加载推迟到首次渲染
import { defineAsyncComponent } from 'vue'
import PanelError from './PanelError.vue'
import PanelLoading from './PanelLoading.vue'
export const AsyncAuditPanel = defineAsyncComponent({
loader: () => import('./AuditPanel.vue'),
loadingComponent: PanelLoading,
errorComponent: PanelError,
delay: 200,
timeout: 5_000,
onError(_error, retry, fail, attempts) {
if (attempts <= 2) retry()
else fail()
}
})defineAsyncComponent 返回一个包装组件。只有它实际进入渲染树时才调用 loader;解析后,props、slots 和 attrs 会传给真正加载到的组件。
加载状态的时间线
| 时刻 | 行为 |
|---|---|
| 首次渲染 | 调用 loader,开始请求 chunk |
| 200ms 内完成 | 直接显示真实组件,避免 loading 闪一下 |
| 超过 200ms | 显示 PanelLoading |
| loader 拒绝 | onError 前两次调用 retry |
| 第三次拒绝 | onError 调用 fail,显示 PanelError |
| 持续未完成 5 秒 | 触发 timeout 错误,显示 PanelError |
重试必须有上限;无限重试会让错误界面永远到不了。loadingComponent 也不应在 delay: 0 下无条件闪现,除非产品明确需要立即反馈。
与 <Suspense> 的控制权冲突
异步组件默认是父级 <Suspense> 的异步依赖。存在 Suspense 祖先时,组件自己的 loadingComponent、errorComponent、delay 和 timeout 不再按局部加载流程生效。Suspense提供pending/fallback协调,但不会自动提供一个error插槽或错误UI;错误需用祖先的 onErrorCaptured 或应用错误处理明确呈现。要么让页面级 Suspense 统一处理,要么在异步组件选项中设 suspensible: false 保留局部控制;不要同时配置两套却期待都生效。
失败实验:可控地拒绝 loader
临时把 loader 改成:
loader: () => Promise.reject(new Error('offline'))观察 attempts 递增、最多重试两次,随后出现错误组件。只把导入路径拼错通常会在构建阶段失败,不适合验证运行时网络错误;可控 Promise 才能稳定重放失败路径。
练习:带草稿缓存和审计懒加载的设置页
实现一个设置页:
ProfilePanel.vue与SecurityPanel.vue同步导入,AuditPanel.vue通过defineAsyncComponent加载。- 使用有类型的
panels注册表和<component :is>,不拼接组件名字符串。 - 用
<KeepAlive :max="3">保留资料页输入草稿;按“资料 → 安全 → 资料”后草稿仍在。 - 审计页提供 200ms 延迟 loading、5 秒 timeout、错误组件和最多两次 retry。
- 用可拒绝 loader 复现错误状态,恢复真实 import 后能正常显示。
参考答案:查看第 2 章第 10 题参考答案。
通关自检:你能分别指出当前组件是谁、当前实例是否被缓存、组件代码是否已经加载,并为三者画出独立状态。