Skip to content

10|动态组件、缓存与异步组件 ​

第二章:组件模型与通信纵览 / 10。<component :is> 选择渲染谁,<KeepAlive> 决定离开时是否销毁,defineAsyncComponent 决定组件代码何时加载;三者不要混成一个“动态加载”。

先只做动态选择 ​

vue
<script setup lang="ts">
import { ref } from 'vue'
const draft = ref('')
</script>

<template>
  <label>昵称 <input v-model="draft" /></label>
</template>
vue
<template>
  <p>安全设置</p>
</template>
vue
<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,也保留组件的静态依赖关系。

切换时实例发生了什么 ​

不加缓存时:

text
profile 首次出现 → 挂载 ProfilePanel,draft=''
输入 Ada         → draft='Ada'
切到 security    → 卸载 ProfilePanel,实例与 draft 消失
切回 profile     → 创建新实例,draft 又是 ''

若产品要求保留未提交草稿,把动态组件放进 <KeepAlive>:

vue
<KeepAlive :max="2">
  <component :is="currentPanel" />
</KeepAlive>

此时离开的实例被停用而不是立即卸载,回来时重新激活。需要暂停计时器或刷新数据的组件,应分别使用 onDeactivated 与 onActivated;不要误以为缓存组件每次回来都会重新 onMounted。

缓存有内存成本。表单本来就应重置、数据必须每次刷新或内容敏感时,不使用 KeepAlive 反而是正确选择。include / exclude 依赖组件 name,SFC 在本课程基线下通常可由文件名推导;重命名文件时要一起检查缓存规则。

在响应式状态中保存组件对象 ​

正文用普通常量注册表,因此组件定义不会被深度代理。若必须直接把当前组件放进响应式引用,使用 shallowRef<Component>;若组件对象嵌在 reactive 集合中,用 markRaw。把组件定义放进普通 ref / reactive 会产生不必要代理和警告。

异步组件把代码加载推迟到首次渲染 ​

ts
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 改成:

ts
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 题参考答案。

通关自检:你能分别指出当前组件是谁、当前实例是否被缓存、组件代码是否已经加载,并为三者画出独立状态。

用心记录每一步成长