WinddSnow

Vue-3-Performance-Optimization-v-once-v-memo-Async-Components-Suspense

字数统计: 6.5k阅读时长: 27 min
2026/08/01

第113课:Vue 性能优化——v-oncev-memo、异步组件、<Suspense>

Vue 3 的响应式系统让开发者能够以声明式的方式编写 UI——当数据变化时,视图自动更新。但在某些场景中,这种“自动追踪”会带来不必要的性能开销:大量静态内容的组件在每次父组件更新时都要重新执行渲染函数;列表中仅少数项目发生变化,但整个列表都被重新求值;大型第三方组件在首屏加载时拖慢了整体速度。Vue 3 提供了一系列精细化的性能优化工具来应对这些问题:**v-once** 将元素或组件标记为“仅渲染一次”并跳过后续的响应式追踪;**v-memo** 根据依赖数组有条件地跳过子树更新;**defineAsyncComponent** 将组件拆分为独立 chunk 并按需加载;**<Suspense>** 协调异步组件的加载状态,提供统一的 fallback 和错误处理。本节课将逐一拆解这些工具的工作原理、适用场景和常见陷阱,并通过一个带图表和日志的仪表板案例展示它们如何协同工作。


1. v-once:仅渲染一次,永不更新

v-once 是一个编译时指令,用于标记元素或组件仅在首次渲染时执行绑定和响应式追踪,之后将其视为静态内容,完全跳过后续的 diff 和更新

1.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
<template>
<div>
<!-- 动态数据,仅计算一次,后续即使 count 变化也不更新 -->
<p v-once>初始计数值:{{ count }}</p>
<button @click="count++">当前计数值:{{ count }}</button>
</div>
</template>

<script setup>
import { ref } from 'vue';
const count = ref(0);
</script>

点击按钮后,count 会递增,但 v-once 绑定的 <p> 标签始终显示“初始计数值:0”。Vue 在首次渲染后将该元素的虚拟 DOM 子树标记为静态,后续 patch 过程中直接跳过。

1.2 性能原理

在 Vue 的编译阶段,模板被编译为渲染函数。v-once 指令会告诉编译器生成一个缓存机制:首次渲染时,执行完整的渲染逻辑并将结果 VNode 缓存;后续渲染时,直接返回缓存的 VNode,不重新创建,不进行依赖收集,不参与 diff。对于包含大量静态文本、永不变化的列表或第三方组件的子树,这可以显著减少虚拟 DOM 的创建和比对的成本。

1.3 适用场景与限制

适用场景 限制
一次性获取的配置数据展示(如用户协议、帮助文档) 被标记的元素及其所有子元素都变为静态
列表中的静态列(如序号列、固定表头) 不能包含任何动态绑定的响应式数据
引入的第三方静态组件(如 Markdown 渲染器) 一旦使用 v-once,该部分永远不会更新
1
2
3
4
5
6
7
8
9
<template>
<!-- ✅ 好:用户协议内容永不变化 -->
<article v-once v-html="termsContent" />

<!-- ❌ 坏:希望列表更新,但使用了 v-once -->
<ul v-once>
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
</ul>
</template>

警告v-once 会使整个子树变为“死树”。如果你后续需要更新其中的任何内容,唯一的办法是销毁并重建整个组件实例。务必确保被标记的内容确实永远不变。


2. v-memo:条件化跳过子树更新

v-memo 是 Vue 3.2 引入的指令,用于有条件地记忆子树。它接收一个依赖数组,仅当数组中的每个值都未变化(使用 === 比较)时,才跳过该元素及其所有后代的重新渲染。如果数组中有任何一个值发生了变化,则重新执行渲染。这与 React 的 useMemo 在组件模板层面的思想类似,但应用在 DOM 元素上。

2.1 基本语法

1
2
3
4
5
6
7
<template>
<div v-memo="[count, name]">
<!-- 仅当 count 或 name 变化时,此子树才会重新渲染 -->
<ExpensiveChild :count="count" :name="name" />
<p>{{ count }} - {{ name }}</p>
</div>
</template>

countname 都未变化时,Vue 会直接复用上一次渲染的 VNode,跳过整个子树的更新流程。这对于包裹渲染开销较大的子组件尤其有用——即使父组件因为其他状态变化而重新渲染,只要 v-memo 的依赖不变,该子树就会保持不变。

2.2 v-memov-for 的组合

v-memo 最常见的用途是优化长列表中的个别项更新。当列表中的大部分数据未变化,仅少数项的某些属性变化时,v-memo 可以避免重新渲染整个列表。

1
2
3
4
5
6
7
8
9
10
11
<template>
<ul>
<li
v-for="item in items"
:key="item.id"
v-memo="[item.id === selectedId]"
>
<ExpensiveItem :item="item" :selected="item.id === selectedId" />
</li>
</ul>
</template>

在这个例子中,v-memo="[item.id === selectedId]" 表示:仅当该项“是否被选中”的状态发生变化时,才重新渲染该项。如果 selectedId1 变为 2,只有 id === 1id === 2 的两个列表项会更新,其余数千项完全跳过渲染。这在带有大量子组件的表格、卡片列表中能带来显著的性能提升。

2.3 v-memov-once 的区别

指令 行为 更新条件 适用场景
v-once 仅渲染一次,之后永远不更新。 永不更新 完全静态的内容。
v-memo 根据依赖数组有条件地跳过更新。 依赖数组中的值变化时更新 大部分时间不变但偶尔需要更新的内容。

选择原则:内容绝对静态 → v-once;内容大多数时候不变但偶有更新 → v-memo


3. 异步组件:按需加载代码

默认情况下,Vue 在构建时会将所有组件打包进同一个 JavaScript 文件。当应用变得庞大时,这会导致首屏加载时间过长。异步组件允许将非首屏必需的组件拆分为独立 chunk,仅在用户触发特定操作(如导航到某个页面、点击展开某个面板)时才从服务器下载。Vue 3 通过 defineAsyncComponent 函数创建异步组件。

3.1 defineAsyncComponent 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineAsyncComponent } from 'vue';

// 简单用法:传入一个返回 Promise 的加载函数
const AsyncChart = defineAsyncComponent(() =>
import('./HeavyChart.vue')
);

// 完整配置:包含加载中、错误和延迟选项
const AsyncModal = defineAsyncComponent({
loader: () => import('./ComplexModal.vue'),
loadingComponent: LoadingSpinner, // 加载中显示的组件
errorComponent: ErrorDisplay, // 加载失败显示的组件
delay: 200, // 延迟显示加载组件(ms),避免闪烁
timeout: 10000, // 超时时间(ms),超时后触发错误
suspensible: true, // 是否可被 <Suspense> 控制(默认 true)
onError(error, retry, fail, attempts) {
// 自定义错误重试逻辑
if (attempts <= 3) {
retry(); // 重试
} else {
fail(); // 放弃,显示 errorComponent
}
},
});

3.2 加载状态与错误处理

defineAsyncComponent 的高级配置提供了三种状态的管理:

1
2
3
4
5
6
7
8
9
10
11
const AsyncPage = defineAsyncComponent({
loader: () => import('./Page.vue'),
loadingComponent: {
template: '<div class="loading-skeleton">加载中,请稍候...</div>'
},
errorComponent: {
template: '<div class="error-card">加载失败,请刷新页面重试</div>'
},
delay: 300, // 300ms 内的加载不显示 loading(避免快速网络下的闪烁)
timeout: 8000,
});

关键参数

  • delay:如果组件加载速度很快(在 delay 毫秒内完成),则不显示 loadingComponent,避免一闪而过的加载状态。默认 200
  • timeout:如果加载超过 timeout 毫秒,触发 onError 并显示 errorComponent。无默认值(永不超时)。
  • onError(error, retry, fail, attempts):自定义错误处理逻辑。retry() 重试加载,fail() 放弃加载并显示错误组件。attempts 是当前已重试次数(从 1 开始)。

3.3 异步组件与路由懒加载的关系

在 Vue Router 中,路由配置直接使用动态 import() 就是一种异步组件:

1
2
3
4
5
6
const routes = [
{
path: '/dashboard',
component: () => import('./Dashboard.vue'), // 这本质上就是异步组件
},
];

Vue Router 内部会调用 defineAsyncComponent 来处理这种动态导入。如果你需要为路由组件添加自定义的加载/错误状态,可以使用 defineAsyncComponent 包裹 import() 后传递给路由。


4. <Suspense>:协调异步依赖的统一容器

<Suspense> 是 Vue 3 提供的内置组件,用于在组件树中协调多个异步依赖的加载状态。它等待所有嵌套的异步组件(以及 async setup()await 的 Promise)准备就绪后,才一次性渲染默认内容。在等待期间,显示 fallback 插槽中的内容。

4.1 基本用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<template>
<Suspense>
<!-- 默认内容:异步组件全部就绪后显示 -->
<template #default>
<AsyncDashboard />
</template>

<!-- 等待期间显示的加载状态 -->
<template #fallback>
<div class="loading">
<Spinner />
<p>正在加载仪表板...</p>
</div>
</template>
</Suspense>
</template>

4.2 工作原理与触发条件

<Suspense> 监听其 default 插槽内部的所有异步依赖。以下情况会被 <Suspense> 识别为“待处理”:

  • 包含使用 defineAsyncComponent 定义的异步组件。
  • 组件的 <script setup> 中使用了 **顶层 await**(即 async setup())。

当所有这些异步依赖都完成后,<Suspense>fallback 替换为 default 内容。如果任何依赖发生错误,可以配合 onErrorCaptured<ErrorBoundary> 模式处理。

4.3 嵌套 Suspense:逐步揭示内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<Suspense>
<template #default>
<!-- 外层:整体页面框架就绪后显示 -->
<PageLayout>
<Suspense>
<template #default>
<!-- 内层:重型图表组件就绪后替换骨架 -->
<AsyncChart />
</template>
<template #fallback>
<ChartSkeleton />
</template>
</Suspense>
</PageLayout>
</template>
<template #fallback>
<FullPageSpinner />
</template>
</Suspense>
</template>

嵌套 <Suspense> 实现了逐步揭示的用户体验:用户先看到页面框架,然后图表区域从骨架屏过渡到真实图表。这比等待所有内容就绪后一次性显示更能让用户感知到“速度”。

4.4 与 v-once / v-memo 的组合

当一个异步组件加载完成后,如果它的内容不会频繁变化,可以结合 v-once 避免后续的响应式追踪:

1
2
3
4
5
6
7
8
9
10
<template>
<Suspense>
<template #default>
<TermsOfService v-once />
</template>
<template #fallback>
<p>加载中...</p>
</template>
</Suspense>
</template>

对于数据面板等偶尔更新的异步组件,可以配合 v-memo 实现精准的更新控制。


5. 其他 Vue 3 性能优化手段

5.1 shallowRefshallowReactive

默认情况下,refreactive深度响应式的——它们会递归地遍历嵌套对象的所有属性并创建代理。对于大型只读数据结构(如从服务端获取的一次性 JSON),这种深度代理会带来不必要的性能开销和内存占用。shallowRefshallowReactive 仅在顶层创建响应式代理,深层嵌套属性不会被追踪。

1
2
3
4
5
6
7
8
9
10
11
12
13
import { shallowRef, shallowReactive } from 'vue';

// shallowRef:仅 .value 本身的替换是响应式的,内部属性变化不触发更新
const data = shallowRef({ items: [...largeArray] });
// 这样修改不会触发渲染(但适合只读数据)
data.value.items[0] = 'new value'; // ❌ 不触发更新
// 这样会触发
data.value = { items: [...] };

// shallowReactive:仅第一层属性是响应式的
const state = shallowReactive({ config: { theme: 'light' } });
state.config.theme = 'dark'; // ❌ 不触发更新(嵌套属性)
state.config = { theme: 'dark' }; // ✅ 触发更新(第一层属性)

使用建议:当你拥有一个只在顶层引用的大型不可变数据集(如地图坐标数据、渲染后的图表数据)时,使用 shallowRef 可以减少深层次的依赖追踪和内存开销。

5.2 合理使用 computed 代替 watch + ref

computed 具有惰性求值和自动缓存机制。如果你需要基于现有状态派生出新值,优先使用 computed 而非在 watch 中手动更新另一个 ref

1
2
3
4
5
6
7
// ❌ 不推荐:手动同步,多维护一个 ref
const count = ref(0);
const double = ref(0);
watch(count, (val) => { double.value = val * 2; });

// ✅ 推荐:使用 computed 自动推导和缓存
const double = computed(() => count.value * 2);

5.3 <KeepAlive>:缓存组件实例

<KeepAlive> 是 Vue 内置的组件缓存机制。当包裹的动态组件切换时,它会将非活跃的组件实例缓存起来,而不是销毁。这对于频繁切换的标签页(Tab)尤为有效——避免每次切换都重新创建组件、重新请求数据、重新执行初始化逻辑。

1
2
3
4
5
<template>
<KeepAlive :max="10">
<component :is="activeTab" />
</KeepAlive>
</template>

<KeepAlive> 支持 include/exclude 按组件名控制缓存,以及 max 限制最大缓存数量(超过时销毁最久未使用的实例)。

5.4 列表中的稳定 key

v-for 提供唯一且稳定的 key 是最基础的性能优化。绝对不要使用 index 作为 key(除非列表是静态且永远不会重新排序的)。使用 index 作为 key 时,如果列表数据发生变化(插入、删除、排序),Vue 会复用错误的 DOM 元素,导致状态错乱(如表单输入内容被错误保留)。使用数据中的唯一 ID(如 item.id)可以确保 Vue 精确地追踪每个元素的身份,最大化 DOM 复用率。


6. Vue 与 React 异步加载的对比

特性 Vue 3 React
异步组件定义 defineAsyncComponent React.lazy
加载状态管理 内置 loadingComponent/errorComponent 需配合 <Suspense> + Error Boundary
延迟/超时/重试 内置 delaytimeoutonError 无内置,需第三方库或手动封装
顶层 await <script setup> 中直接 await 不支持(需通过 Suspense + 数据获取库)
Suspense 集成 <Suspense> 统一管理 <Suspense> 统一管理
列表渲染优化 v-memo 精准控制项级更新 React.memo + useMemo + key

Vue 的异步组件设计更“开箱即用”——defineAsyncComponent 本身就包含了加载、错误、超时和重试的完整配置。React 的 React.lazy 仅处理加载逻辑,错误处理和重试需要开发者自行封装。此外,Vue 3 的 <Suspense> 支持 async setup() 中的顶层 await,这使得数据获取可以在组件层面直接与 Suspense 集成,无需额外的数据获取库(如 React Query/SWR)。


7. 综合实战:优化一个仪表板页面

以下案例将 v-oncev-memodefineAsyncComponent<Suspense> 组合在一个仪表板页面中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
<!-- DashboardPage.vue -->
<script setup>
import { ref, defineAsyncComponent, shallowRef } from 'vue';

// 异步加载重型图表组件
const SalesChart = defineAsyncComponent({
loader: () => import('./SalesChart.vue'),
loadingComponent: { template: '<div class="chart-skeleton">图表加载中...</div>' },
delay: 200,
});

const UserAnalytics = defineAsyncComponent({
loader: () => import('./UserAnalytics.vue'),
loadingComponent: { template: '<div class="chart-skeleton">分析加载中...</div>' },
delay: 200,
});

// 静态的法律声明——永不更新
const termsText = `
<h3>服务条款摘要</h3>
<p>本仪表板数据仅供内部使用,不得对外传播。数据更新频率为每5分钟一次。</p>
`;

// 指标卡片数据
const metrics = ref([
{ id: 'revenue', label: '总收入', value: '¥45,230', change: '+12.5%', up: true },
{ id: 'orders', label: '订单数', value: '1,240', change: '+8.2%', up: true },
{ id: 'users', label: '用户数', value: '3,580', change: '-3.1%', up: false },
{ id: 'rate', label: '转化率', value: '24.5%', change: '+4.7%', up: true },
]);

const selectedMetricId = ref(null);
</script>

<template>
<div class="dashboard">
<!-- 指标卡片:使用 v-memo 仅更新选中的卡片 -->
<section class="metrics-grid">
<div
v-for="metric in metrics"
:key="metric.id"
:class="['metric-card', { selected: selectedMetricId === metric.id }]"
v-memo="[metric.value, selectedMetricId === metric.id]"
@click="selectedMetricId = metric.id"
>
<h3>{{ metric.label }}</h3>
<p class="value">{{ metric.value }}</p>
<span :class="metric.up ? 'up' : 'down'">{{ metric.change }}</span>
</div>
</section>

<!-- 法律声明:使用 v-once 仅渲染一次 -->
<aside v-once class="terms" v-html="termsText" />

<!-- 异步图表:使用 Suspense 统一管理加载状态 -->
<section class="charts">
<Suspense>
<template #default>
<SalesChart />
<UserAnalytics />
</template>
<template #fallback>
<div class="charts-loading">
<div class="chart-skeleton" />
<div class="chart-skeleton" />
</div>
</template>
</Suspense>
</section>
</div>
</template>

<style scoped>
.dashboard { max-width: 1200px; margin: 0 auto; padding: 24px; }
.metrics-grid { display: grid; grid-template-columns: repeat(4, 1fr); gap: 16px; margin-bottom: 24px; }
.metric-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); cursor: pointer; transition: box-shadow 0.2s; }
.metric-card.selected { box-shadow: 0 0 0 2px #4f46e5; }
.metric-card h3 { font-size: 0.875rem; color: #666; margin: 0 0 8px; }
.metric-card .value { font-size: 1.5rem; font-weight: bold; margin: 0 0 4px; }
.metric-card .up { color: #10b981; }
.metric-card .down { color: #ef4444; }
.terms { background: #f9f9f9; padding: 16px; border-radius: 8px; margin-bottom: 24px; font-size: 0.875rem; color: #666; }
.charts { display: grid; grid-template-columns: 1fr 1fr; gap: 24px; }
.chart-skeleton { height: 300px; background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%); background-size: 200% 100%; animation: shimmer 1.5s infinite; border-radius: 8px; }
@keyframes shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } }
</style>

优化点总结

  • v-memo:四个指标卡片中,每次仅更新被选中状态变化的卡片(以及数据变化的卡片),其他卡片跳过渲染。
  • v-once:法律声明文本从服务端获取后永不变化,标记为静态内容,后续渲染完全不参与 diff。
  • defineAsyncComponentSalesChartUserAnalytics 两个重型图表组件被拆分为独立 chunk,仪表板首屏加载时不下载它们,仅在即将进入视口时才加载。
  • <Suspense>:统一管理两个异步图表的加载状态,显示统一的骨架屏。
  • shallowRef:适合存储大型只读数据集,减少深度追踪开销(在本例中未展示,但在数据量极大的场景中应考虑)。

课后练习

一、概念自测(选择题 / 填空题)

  1. (单选) v-once 指令的作用是?
    A. 在依赖变化时仅更新一次。
    B. 将元素或组件标记为仅渲染一次,之后永不更新。
    C. 延迟元素的首次渲染。
    D. 在条件为真时渲染一次。

  2. (单选) v-memo="[count, name]" 的行为是?
    A. 当 countname 变化时,重新渲染该元素。
    B. 无论 countname 是否变化,该元素永不更新。
    C. 仅当 countname 都未变化时,跳过该元素的更新。
    D. 与 v-once 行为完全相同。

  3. (填空) 在 Vue 3 中创建可配置加载状态和错误重试的异步组件,应使用 ______ 函数。

  4. (多选) 关于 <Suspense> 的描述,哪些是正确的?
    A. 它可以等待所有嵌套的异步组件就绪后才渲染默认内容。
    B. 它仅支持等待 defineAsyncComponent 创建的异步组件。
    C. 它的 fallback 插槽用于显示加载状态。
    D. 它支持嵌套使用,实现逐步揭示内容。

二、AI 编程任务:编写面向 AI 的提示词

场景:你需要实现一个优化的 Vue 3 产品列表页面。要求如下:

  • 页面包含一个顶部的“促销公告”区域,内容从服务端获取后不再变化,使用 v-once 渲染。
  • 产品列表包含 1000 个产品卡片,每个卡片有一个“加入购物车”按钮。当用户点击按钮时,只有被点击的卡片更新(按钮变为“已添加”),其余卡片不重新渲染。使用 v-memo 实现。
  • 页面底部有一个“用户评价”区域,该区域是一个重型组件(包含大量动画和交互),使用 defineAsyncComponent 异步加载,配合 <Suspense> 显示骨架屏。
  • 产品数据存储在 shallowRef 中(因为是只读的大型 JSON),避免深层响应式追踪。

任务要求:请写出一段完整的中文提示词,发送给 AI,使其生成符合上述要求的 Vue 3 单文件组件。提示词中需明确指定 v-oncev-memodefineAsyncComponent<Suspense>shallowRef 的使用位置和方式。

三、Agent 模式下的提示词示例

你是一个资深前端开发 Agent。请创建一个 Vue 3 优化版产品列表页面。需要创建以下文件:

  1. src/pages/ProductList.vue
    • 使用 <script setup>
    • 顶部促销公告:<div v-once v-html="promoHtml" />promoHtml 是一个包含促销信息的 HTML 字符串(ref,但从 API 获取后不再变化)。
    • 产品列表:shallowRef 存储 1000 个产品对象({ id, name, price, addedToCart })。使用 v-for 渲染,每个卡片绑定 v-memo="[item.addedToCart]",确保仅当某个产品的 addedToCart 状态变化时才重新渲染该卡片。点击“加入购物车”按钮时更新对应产品的 addedToCarttrue 并修改按钮文字为“✓ 已添加”。
    • 底部用户评价区域:使用 defineAsyncComponent 异步加载 UserReviews.vue,配置 loadingComponent 为一个骨架屏组件(ReviewSkeleton),delay 200ms。使用 <Suspense> 包裹,fallback 插槽中放置骨架屏。
    • 所有产品卡片样式为一个简单的网格布局。添加 JSDoc 注释。
  2. src/components/UserReviews.vue:一个简单组件(可仅展示静态评价文字或模拟异步数据加载,使用顶层 await 延迟 1 秒后渲染)。
  3. src/components/ReviewSkeleton.vue:骨架屏组件(灰色占位块)。
    完成后列出所有文件内容。

四、面试真题与参考答案

题目(字节跳动前端面试题):

请详细对比 Vue 3 的异步组件(defineAsyncComponent)和 React 的 React.lazy。Vue 3 的 <Suspense> 与 React 的 <Suspense> 在设计和使用上有何异同?Vue 3 的 <Suspense> 如何与 async setup() 协同工作?这在 React 中是否有等价实现?

参考答案

异步组件对比

  • Vue 3 的 defineAsyncComponent 内置了加载状态(loadingComponent)、错误状态(errorComponent)、延迟显示(delay)、超时(timeout)和自定义重试逻辑(onError),开发者只需传入配置即可。
  • React 的 React.lazy 仅负责动态导入和懒加载,不提供错误处理、超时和重试机制。开发者需要额外创建 Error Boundary 组件来捕获加载失败,并使用 <Suspense>fallback 处理加载状态。React 的方案更“原子化”,但需要更多的样板代码。

<Suspense> 对比

  • 两者都提供了一个声明式的 fallback 加载状态,等待子树中的异步组件就绪后渲染默认内容。
  • Vue 3 的 <Suspense> 额外支持 async setup()——组件的 setup 函数可以是 async 的,内部使用顶层 await。这允许组件在数据准备就绪之前“暂停”渲染,<Suspense> 会在此期间显示 fallback。React 不支持组件的异步渲染函数本身(组件必须是同步函数),数据获取通常需要配合外部库(如 React Query、SWR)和 <Suspense> 通过 throw Promise 的机制间接实现。
  • Vue 3 的 <Suspense> 还支持嵌套使用,允许开发者构建逐步揭示的加载体验,而 React 的嵌套 <Suspense> 同样支持此模式。

async setup() 的协同:在 Vue 3 中,如果一个组件的 <script setup> 中使用了顶层 await(例如 const data = await fetch('/api/data')),Vue 会将此组件视为异步依赖。当该组件被 <Suspense> 包裹时,<Suspense> 会等待这个 await 完成后再渲染默认内容。React 中没有直接等价实现——React 组件本身必须是同步函数。React 社区通过允许组件的 render 函数抛出 Promise(由 <Suspense> 捕获)来模拟类似行为,但这通常由数据获取库封装,开发者一般不直接写这种代码。


课后练习答案

一、概念自测答案

  1. B

    • 解析:v-once 使元素或组件仅渲染一次,之后永不更新。A 描述错误,C 和 D 不存在这些行为。
  2. C

    • 解析:v-memo 的依赖数组中的值如果都未变化(===),则跳过该元素及其子树的更新。A 描述的是触发更新的条件,B 和 D 错误。
  3. defineAsyncComponent

    • 解析:defineAsyncComponent 是 Vue 3 提供的创建异步组件的函数,支持加载状态、错误处理和重试。
  4. A、C、D

    • 解析:B 错误,<Suspense> 不仅支持 defineAsyncComponent,也支持 async setup() 中的顶层 await。A、C、D 均为正确描述。

二、AI 编程任务参考答案(提示词示例)

示例提示词
“请用 Vue 3 <script setup> 创建一个优化版产品列表页面。要求:

  • 促销公告使用 v-once 渲染(内容一次性获取)。
  • 1000 个产品卡片,shallowRef 存储数据。每张卡片 v-memo="[item.addedToCart]",点击按钮更新对应产品的 addedToCart 状态。
  • 底部评价区域使用 defineAsyncComponent + <Suspense> 异步加载,骨架屏作为 fallback。
  • 输出完整单文件组件。”
CATALOG
  1. 1. 第113课:Vue 性能优化——v-once、v-memo、异步组件、<Suspense>
    1. 1.1. 1. v-once:仅渲染一次,永不更新
      1. 1.1.1. 1.1 基本用法
      2. 1.1.2. 1.2 性能原理
      3. 1.1.3. 1.3 适用场景与限制
    2. 1.2. 2. v-memo:条件化跳过子树更新
      1. 1.2.1. 2.1 基本语法
      2. 1.2.2. 2.2 v-memo 与 v-for 的组合
      3. 1.2.3. 2.3 v-memo 与 v-once 的区别
    3. 1.3. 3. 异步组件:按需加载代码
      1. 1.3.1. 3.1 defineAsyncComponent 基本用法
      2. 1.3.2. 3.2 加载状态与错误处理
      3. 1.3.3. 3.3 异步组件与路由懒加载的关系
    4. 1.4. 4. <Suspense>:协调异步依赖的统一容器
      1. 1.4.1. 4.1 基本用法
      2. 1.4.2. 4.2 工作原理与触发条件
      3. 1.4.3. 4.3 嵌套 Suspense:逐步揭示内容
      4. 1.4.4. 4.4 与 v-once / v-memo 的组合
    5. 1.5. 5. 其他 Vue 3 性能优化手段
      1. 1.5.1. 5.1 shallowRef 与 shallowReactive
      2. 1.5.2. 5.2 合理使用 computed 代替 watch + ref
      3. 1.5.3. 5.3 <KeepAlive>:缓存组件实例
      4. 1.5.4. 5.4 列表中的稳定 key
    6. 1.6. 6. Vue 与 React 异步加载的对比
    7. 1.7. 7. 综合实战:优化一个仪表板页面
    8. 1.8. 课后练习
      1. 1.8.1. 一、概念自测(选择题 / 填空题)
      2. 1.8.2. 二、AI 编程任务:编写面向 AI 的提示词
      3. 1.8.3. 三、Agent 模式下的提示词示例
      4. 1.8.4. 四、面试真题与参考答案
    9. 1.9. 课后练习答案
      1. 1.9.1. 一、概念自测答案
      2. 1.9.2. 二、AI 编程任务参考答案(提示词示例)