概述
<Suspense> 是 Vue 3 引入的一个实验性 内置组件,用于协调异步依赖的加载状态。它允许我们在组件树中等待嵌套的异步依赖被解析时,统一展示 fallback 内容。
⚠️ 注意 :Suspense 目前仍是实验性特性(自 Vue 3.0 起),API 可能在未来版本中发生变化。生产环境使用需谨慎。
本文会从基本用法讲起,逐步深入到嵌套、Router 集成、错误处理,最后列出所有你需要注意的坑。
基本用法
1. 最简单的 <Suspense>
vue
<template>
<Suspense>
<!-- 默认插槽:异步内容 -->
<AsyncComponent />
<!-- fallback 插槽:加载中 -->
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>
当 AsyncComponent 中的异步操作完成前,显示 fallback 内容;完成后自动切换为默认插槽内容。
2. 异步组件 + Suspense
vue
<script setup>
import { defineAsyncComponent, ref } from 'vue'
const AsyncDashboard = defineAsyncComponent(() =>
import('./components/AsyncDashboard.vue')
)
</script>
<template>
<Suspense>
<AsyncDashboard />
<template #fallback>
<LoadingSkeleton />
</template>
</Suspense>
</template>
defineAsyncComponent 配合 Suspense 可以实现组件级别的懒加载 + 加载态 ,比单独使用 defineAsyncComponent 的 loadingComponent 选项更灵活。
3. <script setup> 中的顶层 await
这是 Suspense 最强大的场景------在 <script setup> 中直接 await:
vue
<!-- UserProfile.vue -->
<script setup>
const { data: user } = await fetch('/api/user').then(r => r.json())
const { data: posts } = await fetch(`/api/users/${user.id}/posts`).then(r => r.json())
</script>
<template>
<div>
<h1>{{ user.name }}</h1>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
vue
<!-- App.vue -->
<template>
<Suspense>
<UserProfile />
<template #fallback>
<div>Loading profile...</div>
</template>
</Suspense>
</template>
⚡ 关键点 :使用
<script setup>+await的组件必须 被<Suspense>包裹,否则 Vue 会抛出警告。
事件与生命周期
Suspense 接受两个 props,触发三个事件:
Props:
| Prop | 类型 | 说明 |
|---|---|---|
timeout |
`string | number` |
suspensible |
boolean |
嵌套 Suspense 时,内部组件设置此属性将异步依赖处理交给父级(Vue 3.3+) |
Events:
| 事件 | 触发时机 |
|---|---|
pending |
进入挂起状态时 |
resolve |
默认插槽的异步依赖全部解决时 |
fallback |
显示 fallback 内容时 |
vue
<Suspense
@pending="onPending"
@resolve="onResolve"
@fallback="onFallback"
>
<AsyncComponent />
<template #fallback>
<Loading />
</template>
</Suspense>
嵌套 Suspense
Suspense 是可以嵌套的。内部 Suspense 需显式声明 suspensible 属性(Vue 3.3+),否则会被父 Suspense 视为同步组件,导致渲染异常:
vue
<Suspense>
<component :is="DynamicAsyncOuter">
<!-- ⭐ 内部 Suspense 必须加 suspensible -->
<Suspense suspensible>
<component :is="DynamicAsyncInner" />
</Suspense>
</component>
<template #fallback>
<FullPageLoader />
</template>
</Suspense>
设置 suspensible 后,所有异步依赖处理都交给父级 Suspense,内部仅充当补丁边界。
如果不加 suspensible,当内外两个组件同时变化时,可能出现空节点和多次修补周期。
结合 Router 使用
Vue Router 可与 Suspense、Transition、KeepAlive 组合使用,嵌套顺序很重要:
vue
<template>
<router-view v-slot="{ Component }">
<template v-if="Component">
<Transition mode="out-in">
<KeepAlive>
<Suspense>
<component :is="Component" />
<template #fallback>
正在加载...
</template>
</Suspense>
</KeepAlive>
</Transition>
</template>
</router-view>
</template>
⚠️ Vue Router 使用动态导入做的路由懒加载 (
() => import(...))与异步组件不同 ,它们目前不会触发<Suspense>。但如果路由组件内部有异步组件作为后代,这些后代仍可正常触发 Suspense。
与组合式函数 (Composables) 结合
可以将异步数据获取封装成 composable,在 <script setup> 中直接 await:
vue
<!-- composables/useUserData.js -->
export async function useUserData(userId) {
const user = await fetch(`/api/user/${userId}`).then(r => r.json())
const posts = await fetch(`/api/user/${userId}/posts`).then(r => r.json())
return { user, posts }
}
vue
<!-- UserPage.vue -->
<script setup>
const props = defineProps({ userId: String })
const { user, posts } = await useUserData(props.userId)
</script>
<template>
<div>
<h2>{{ user.name }}</h2>
<div v-for="post in posts" :key="post.id">{{ post.title }}</div>
</div>
</template>
这种写法极大地简化了异步数据流------不需要 ref、onMounted、loading 状态等样板代码。
错误处理
<Suspense> 自身不提供错误处理。需要在父组件中使用 onErrorCaptured() 钩子捕获异步错误:
vue
<script setup>
import { onErrorCaptured } from 'vue'
onErrorCaptured((err) => {
console.error('Suspense 异步错误:', err)
return false // 阻止错误继续传播
})
</script>
<template>
<Suspense>
<AsyncComponent />
<template #fallback>加载中...</template>
</Suspense>
</template>
也可以使用选项式 API 的
errorCaptured选项达到同样效果。
注意事项与陷阱
1. 实验性特征 ⚠️
Suspense 的 API 尚未完全稳定,Vue 官方文档明确标注为实验性,在 Vue 3.3+ 中有所改进但仍未转正。
2. 不要在 Suspense 内使用 onMounted
在 Suspense 的默认插槽组件中,onMounted 可能在 Suspense 解析前触发。如果需要在组件真正挂载后执行副作用,使用 onActivated 或 onDeactivated(如果组件被 <KeepAlive> 包裹)。
3. 不要在 Suspense 内直接使用条件渲染
vue
<!-- ❌ 错误写法 -->
<Suspense>
<component :is="someCondition ? AsyncA : AsyncB" />
<template #fallback>...</template>
</Suspense>
如果条件在 Suspense 解析后改变,Suspense 不会重新进入挂起状态,需要手动切换。建议将条件判断放在 Suspense 内部处理。
4. Suspense 只解析一次
一旦 Suspense 解析完成(显示默认插槽),如果内部的异步组件因条件变化被替换,Suspense 不会 再次显示 fallback。需要手动控制时,可结合 key 属性强制重新创建 Suspense:
vue
<Suspense :key="route.fullPath">
<RouterView />
<template #fallback>...</template>
</Suspense>
5. 服务端渲染 (SSR) 行为差异
SSR 环境下,Suspense 的默认插槽内容会在服务端同步渲染,不会等待异步依赖。服务端需要异步数据时应使用 onServerPrefetch 或 Nuxt 的 asyncData/useFetch 等方案。
完整示例:用户主页
vue
<!-- UserHome.vue -->
<script setup>
import { ref, defineAsyncComponent } from 'vue'
const showProfile = ref(false)
const UserProfile = defineAsyncComponent(() =>
import('./UserProfile.vue')
)
</script>
<template>
<div class="home">
<button @click="showProfile = true">查看用户信息</button>
<!-- 用 key 确保每次点击都重新触发 Suspense -->
<Suspense :key="showProfile">
<div v-if="showProfile">
<UserProfile />
</div>
<template #fallback>
<div class="skeleton">
<div class="skeleton-avatar"></div>
<div class="skeleton-name"></div>
<div class="skeleton-list">
<div v-for="n in 3" :key="n" class="skeleton-item"></div>
</div>
</div>
</template>
</Suspense>
</div>
</template>
vue
<!-- UserProfile.vue -->
<script setup>
// 模拟两个串行请求
const user = await fetch('https://jsonplaceholder.typicode.com/users/1')
.then(res => res.json())
const posts = await fetch(
`https://jsonplaceholder.typicode.com/users/${user.id}/posts`
).then(res => res.json())
</script>
<template>
<div class="profile">
<h2>{{ user.name }}</h2>
<p>{{ user.email }} · {{ user.website }}</p>
<h3>文章 ({{ posts.length }})</h3>
<ul>
<li v-for="post in posts" :key="post.id">{{ post.title }}</li>
</ul>
</div>
</template>
总结
| 特性 | 说明 |
|---|---|
| ✅ 简化异步代码 | 无需手动管理 loading 状态 |
| ✅ 自然的数据流 | <script setup> 中直接 await |
| ✅ 组合灵活 | 可嵌套,可与 Router/KeepAlive/Transition 搭配 |
| ✅ errorCaptured 错误处理 | 自身无错误处理,需父组件用 onErrorCaptured |
| ⚠️ 实验性 API | 生产环境使用需关注版本更新 |
| ⚠️ 只解析一次 | 需配合 key 手动触发重新挂起 |
| ⚠️ 仅允许一个直接子节点 | 默认插槽只允许一个直接子节点 |
| ⚠️ 路由懒加载不触发 | Vue Router 的 ()=>import() 不触发 Suspense |
Suspense 改变了 Vue 处理异步组件的方式,让声明式异步 成为可能。虽然目前仍是实验性功能,但它背后"声明等待状态"的理念已经非常清晰------我们不再需要手写 v-if="loading",而是让框架帮我们管理这个过程。
参考资料
如果本文对你有帮助,欢迎 点赞 👍、收藏 ⭐、关注 🔔,你的支持是我持续输出的动力!
有什么问题或想法?欢迎在评论区留言讨论 💬