Vue3 零基础每日笔记(032):Suspense 与异步组件——懒加载与顶层 await 终极方案

一、前言

011 篇埋过伏笔:"<script setup> 顶层裸 await 会白屏,Suspense 032 篇细讲"------今天兑现。本篇两条线:一是 defineAsyncComponent 异步组件 ,让重型组件不拖累首屏;二是 Suspense,给异步组件配统一的"加载中/出错了"外壳。学完这篇,首屏优化和异步加载就都有了标准答案。

二、defineAsyncComponent:用到才加载

重型组件(图表、富文本)在页面加载时就全部打包,首屏白等。defineAsyncComponent 把它变成"用到时才去下载":

vue 复制代码
<!-- App.vue -->
<template>
  <button @click="showChart = true">打开图表</button>

  <!-- 点击后才真正下载 BigChart.vue 的代码 -->
  <BigChart v-if="showChart" />
</template>

<script setup>
import { ref, defineAsyncComponent } from 'vue'

// 用一个"返回 import() 的函数"定义异步组件
const BigChart = defineAsyncComponent(() =>
  import('./components/BigChart.vue')
)

const showChart = ref(false)
</script>

原理一句话:import('./components/BigChart.vue') 是动态导入,Vite 会把它单独拆成一个 js 文件(代码分割),首次渲染 BigChart 组件时才发起网络请求加载。首页的包里没有它,首屏自然快。

【截图位置:F12 Network 里点击后才出现 chart 组件 js 请求的截图】

三、带加载态与错误态的异步组件

网络慢时用户看什么?defineAsyncComponent 支持完整配置:

vue 复制代码
<!-- LoadingSpin.vue -->
<template><p class="loading">⏳ 组件加载中...</p></template>

<style scoped>
.loading { color: #999; padding: 20px; }
</style>
vue 复制代码
<!-- LoadError.vue -->
<template><p class="err">❌ 加载失败,请检查网络后重试</p></template>

<style scoped>
.err { color: #e74c3c; padding: 20px; }
</style>
vue 复制代码
<!-- App.vue -->
<template>
  <BigChart />
</template>

<script setup>
import { defineAsyncComponent } from 'vue'
import LoadingSpin from './components/LoadingSpin.vue'
import LoadError from './components/LoadError.vue'

const BigChart = defineAsyncComponent({
  loader: () => import('./components/BigChart.vue'),
  loadingComponent: LoadingSpin,  // 加载中显示
  errorComponent: LoadError,      // 失败显示
  delay: 200,      // 200ms 内加载完就不闪 loading(防抖体验)
  timeout: 5000    // 5 秒超时,切到 errorComponent
})
</script>

delay: 200 是个贴心的体验细节:网络快时 loading 一闪而过反而像 bug,200ms 内加载完成就完全不显示。

四、Suspense:给异步 setup 一个统一外壳

另一种异步形态:组件内部顶层 await(等接口数据再渲染)。011 篇说过裸写会白屏------因为父组件不知道它还没准备好。Suspense 就是官方的"等待外壳":

vue 复制代码
<!-- src/components/UserProfile.vue:顶层 await 的异步组件 -->
<template>
  <div class="profile">
    <h4>{{ user.name }}</h4>
    <p>{{ user.bio }}</p>
  </div>
</template>

<script setup>
// ⚠️ 顶层 await:必须配合 Suspense 使用
const user = await new Promise((resolve) => {
  setTimeout(() => resolve({ name: '小明', bio: '三年前端' }), 1500)
})
</script>
vue 复制代码
<!-- App.vue -->
<template>
  <Suspense>
    <!-- default 插槽:真正的内容(内部只能放一个根节点) -->
    <UserProfile />

    <!-- fallback 插槽:等待期间显示的占位 -->
    <template #fallback>
      <p>资料加载中...</p>
    </template>
  </Suspense>
</template>

<script setup>
import UserProfile from './components/UserProfile.vue'
</script>

执行流程:挂载 → UserProfile 的 setup 开始 await → Suspense 检测到"没就绪"→ 显示 fallback → 1.5 秒后 await 完成 → 切换显示真正内容。

注意实验状态:Suspense 至今仍是实验性 API,行为可能微调,生产环境用前评估;但它解决"顶层 await"这个问题没有替代品,理解思想即可。

【截图位置:fallback 占位与加载完成后内容的切换效果】

五、Suspense 配合错误处理

异步也可能失败,Suspense 体系的标准姿势:外层 onErrorCaptured 捕获 + 默认插槽里 v-if 切换:

vue 复制代码
<!-- App.vue(错误处理版) -->
<template>
  <Suspense v-if="!failed" @fallback="state = 'loading'">
    <UserProfile @error="failed = true" />
    <template #fallback>
      <p>资料加载中...</p>
    </template>
  </Suspense>
  <p v-else>❌ 资料加载失败,请刷新重试</p>
</template>

<script setup>
import { ref, onErrorCaptured } from 'vue'
import UserProfile from './components/UserProfile.vue'

const failed = ref(false)

// 捕获后代组件(含异步 setup)抛出的错误
onErrorCaptured((err) => {
  console.error('捕获到异步组件错误:', err)
  failed.value = true
  return false   // 阻止继续向上抛
})
</script>

对应地,UserProfile 里抛错(模拟接口失败):

js 复制代码
const user = await new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error('接口 500')), 1500)
})

错误链路:子组件 reject → Suspense 边界 → onErrorCaptured 捕获 → v-if 切到错误提示。真实项目的错误兜底骨架就是这样。

六、两条线的分工与配合

方案 解决什么 适用
defineAsyncComponent 按需下载组件代码,减首屏体积 图表、编辑器等重组件
Suspense 等待异步 setup,统一加载/错误外壳 顶层 await 的数据组件

两者可以叠:异步组件 + 内部 await + 父级 Suspense 都成立。另外路由篇(050)的"路由懒加载"用的就是今天第二节同款 () => import()------这里学会,那里白捡。

七、踩坑记录

  1. 顶层裸 await 没包 Suspense:组件和它以上整棵树都不渲染(白屏)------顶层 await 与 Suspense 是绑定关系,二选一不用就都别用
  2. Suspense default 插槽放了多个根 :行为未定义,只放一个组件;多个组件各自包一层再组合
  3. defineAsyncComponent 写成了 defineAsyncComponent(import('...')) :少了箭头函数!必须传"函数",让 Vue 决定什么时候调用------() => import(...)
  4. delay 设 0 导致 loading 闪烁:本地网络毫秒级加载完,loading 一闪像 bug------给 200ms 左右的缓冲
  5. onErrorCaptured 忘了返回 false:错误继续向上抛到全局,控制台红字刷屏------处理后返回 false 静默

八、今日小结

  • defineAsyncComponent + () => import()代码分割按需加载,delay/timeout/loading/error 四件套配置
  • Suspense:等待顶层 await,#default 放内容、#fallback 放占位(实验性 API)
  • onErrorCaptured 捕获异步错误,返回 false 停止上抛
  • 首屏优化第二课完成(第一课是 014 浅层响应式),路由懒加载提前解锁

下篇预告

组件 API 清单还剩最后几个"暗器":透传属性 $attrs、拿到 DOM 和组件实例的 ref、子组件开门 defineExpose。下一篇 033 attrs 与 refs------组件通信的收官之战。

相关推荐
Alice-YUE1 小时前
前端速通 Agent:5 个项目,一条学习路径
前端·大模型·ai agent·学习路径·deerflow
今儿敲了吗1 小时前
04英文文本关键词提取(TF-IDF)
笔记·python
志尊宝1 小时前
Vue3 零基础每日笔记(035):什么是 Composables——mixin 之死与逻辑复用新方案
笔记·vue·html·前端开发·软件开发
是立不是利1 小时前
前端分页逻辑与边界场景实战解析
前端·javascript
小聪7081 小时前
elpis-core 动态组件扩展设计
前端
Amos_Web2 小时前
Rspack 源码解析(七):Module、Chunk 与加载树优化
前端·rust·源码阅读
动词ing2 小时前
【学习笔记】数据结构 二分查找(二分查找模板+边界处理+查找区间+旋转数组)
笔记·学习
OpenTiny社区2 小时前
从开发到运行:OpenTiny NEXT 如何构建 Agent 时代的 Web 应用?
前端·github·ai编程
kkkkkkkkkk_Z2 小时前
学嵌入式和Linux应用编程|学习日记:深入理解TCP协议与网络编程实战
linux·笔记·学习