官方文档:
<Suspense>
指南-Suspense
官方提示:
<Suspense>
是一项实验性功能。它不一定会最终成为稳定功能,并且在稳定之前相关 API 也可能会发生变化。
<Suspense>
是一个内置组件,用来在组件树中协调对异步依赖的处理。它让我们可以在组件树上层等待下层的多个嵌套异步依赖项解析完成,并可以在等待时渲染一个加载状态。
异步依赖
要了解 <Suspense>
所解决的问题和它是如何与异步依赖进行交互的,需要了解什么是异步依赖。
<Suspense>
主要用于包裹异步组件,这些组件可能需要进行异步数据获取或其他耗时的操作:
<Suspense>
└─ <Dashboard>
├─ <Profile>
│ └─ <FriendStatus>(组件有异步的 setup())
└─ <Content>
├─ <ActivityFeed> (异步组件)
└─ <Stats>(异步组件)
在这个组件树中有多个嵌套组件,要渲染出它们,首先得解析一些异步资源。
- 如果没有
<Suspense>
组件:- 以vue2 项目为例:这个组件树中的全部组件,都需要处理各自的加载、报错和完成状态。在网络不好或数据量过大的时候,可以看到页面上有多个加载状态,它们在不同的时间加载完成,页面显示出内容。
- 有了
<Suspense>
组件后:- 可以在等待整个多层级组件树中的各个异步依赖获取结果时,在顶层展示出加载中或加载失败的状态。
<Suspense>
可以等待的异步依赖有两种:
-
带有异步
setup()
钩子的组件。这也包含了使用<script setup>
时有顶层await
表达式的组件。 -
异步组件。
async setup()
组合式 API 中组件的 setup()
钩子可以是异步的:
export default {
async setup() {
const res = await fetch(...)
const posts = await res.json()
return {
posts
}
}
}
<script setup>
语法糖
在<script setup>
语法糖中,开发者无法在前面添加async
。
如果使用 <script setup>
,那么顶层 await
表达式会自动让该组件成为一个异步依赖:
<!-- Child.vue -->
<template>
<div class="child-box">
<h1>子组件</h1>
<h2>网易热歌榜</h2>
</div>
</template>
<script setup lang="ts">
// 获取网易云热歌榜
let res = await fetch('https://api.uomg.com/api/rand.music?sort=热歌榜&format=json')
console.log(res.json()) // 一个Promise
</script>
在父组件中直接引用Child.vue
组件:
<template>
<div class="home-wrap">
<h1>主页----父组件</h1>
<Child />
</div>
</template>
<script setup lang="ts">
import Child from './Child.vue';
</script>
页面没有显示Child.vue
的内容,并且在控制台提示:
[Vue warn]: Component : setup function returned a promise, but no boundary was found in the parent component tree. A component with async setup() must be nested in a in order to be rendered.
setup
函数返回了一个promise
,但是在父组件树中没有找到<Suspense>
边界。带有async setup()
的组件必须嵌套在<Suspense>
中才能呈现。
按照提示修改父组件:
<template>
<div class="home-wrap">
<h1>主页----父组件</h1>
<Suspense>
<!-- 异步任务ok后 -->
<template v-slot:default>
<Child />
</template>
<!-- 异步任务未做完时 -->
<template v-slot:fallback>
<div>请求中...</div>
</template>
</Suspense>
</div>
</template>
<script setup lang="ts">
import { Suspense } from 'vue';
import Child from './Child.vue';
</script>
页面成功展示Child.vue
的内容:
异步组件
异步组件默认就是suspensible: true
的。这意味着如果组件关系链上有一个 <Suspense>
,那么这个异步组件就会被当作这个 <Suspense>
的一个异步依赖。在这种情况下,加载状态是由 <Suspense>
控制,而该组件自己的加载、报错、延时和超时等选项都将被忽略。
异步组件也可以通过在选项中指定 suspensible: false
表明不用 Suspense
控制,并让组件始终自己控制其加载状态。
<Suspense>
props
interface SuspenseProps {
timeout?: string | number
suspensible?: boolean
}
参数说明:
timeout
:可选参数。- 类型:
string | number
- 作用:指定一个超时时间。如果异步依赖在这个时间内没有解析完成,
<Suspense>
将不再等待并显示fallback
内容。
- 类型:
suspensible
:可选参数。- 类型:
boolean
,默认值为false
。 - 作用:当设置为
true
时,组件树中所有的异步依赖将由父级 Suspense 处理。这对于构建复杂的异步加载场景非常有用,可以将多个异步依赖的处理集中在一个更高层次的<Suspense>
组件中。
- 类型:
插槽
<Suspense>
组件接受两个插槽:#default
和 #fallback
。两个插槽都只允许一个直接子节点。
#default
插槽用于放置异步依赖的组件或内容。- 如果在渲染时遇到异步组件或具有
async setup()
的组件,<Suspense>
会等待所有异步依赖项解析完成后再显示#default
插槽的内容。
- 如果在渲染时遇到异步组件或具有
#fallback
:在异步依赖解析完成之前,<Suspense>
会显示#fallback
插槽中的内容。
<Suspense>
<!-- 具有深层异步依赖的组件 -->
<template #fallback>
<Dashboard />
</template>
<!-- 在 #fallback 插槽中显示 “正在加载中” -->
<template #fallback>
Loading...
</template>
</Suspense>
-
初始渲染
- 在初始渲染时,
<Suspense>
会开始在内存中渲染#default
插槽的内容。 - 如果
<Dashboard />
组件(或者它的子组件树)中包含异步依赖,比如异步组件加载或者async setup()
函数,那么在处理这些异步依赖的过程中,<Suspense>
会进入挂起状态。 - 例如,假设
<Dashboard />
组件内部,有一个异步请求数据的方法,在数据请求期间,<Suspense>
就会进入挂起状态。
- 在初始渲染时,
-
挂起状态
- 在挂起状态期间,
<Suspense>
会展示#fallback
插槽中的内容,以向用户提供反馈,表明正在进行一些异步操作。 - 例如,当
<Dashboard />
中的异步依赖正在处理时,用户会看到Loading...
。
- 在挂起状态期间,
-
完成状态
- 当所有的异步依赖都完成后,
<Suspense>
会进入完成状态,并显示#default
插槽的内容。 - 如果在初次渲染时没有遇到异步依赖,
<Suspense>
会直接进入完成状态,直接展示#default
插槽的内容。
- 当所有的异步依赖都完成后,
状态切换条件:
官方说明:进入完成状态后,只有当默认插槽的根节点被替换时,
<Suspense>
才会回到挂起状态。组件树中新的更深层次的异步依赖不会造成<Suspense>
回退到挂起状态。
-
一旦进入完成状态,只有当
#default
插槽的根节点被替换时,<Suspense>
才会回到挂起状态。- 例如,如果
<Dashboard />
组件被完全替换为另一个具有异步依赖的组件,那么<Suspense>
会重新进入挂起状态。 - 当
<Suspense>
从完成状态因为#default
插槽根节点被替换而回退到挂起状态时,后备内容不会立即展示。
而是在等待新内容和异步依赖完成的过程中,仍然展示之前#default
插槽的内容。 - 通过设置
timeout
属性,配置过度行为:- 在等待渲染新内容耗时超过
timeout
之后,<Suspense>
将会切换为展示后备内容。 - 设置
timeout: 0
,一旦#default
插槽的根节点被替换,就会立即显示后备内容。
- 在等待渲染新内容耗时超过
- 例如,如果
-
如果只是在
<Dashboard />
组件的子组件树中添加了新的异步依赖,<Suspense>
不会回退到挂起状态。
事件
<Suspense>
组件会触发三个事件:pending
、resolve
和 fallback
。
@pending
是在进入挂起状态时触发。- 当异步依赖开始加载时触发。可以在这个事件处理函数中显示加载指示器等。
@resolve
是在default
插槽完成获取新内容时触发。- 当所有异步依赖项解析完成时触发。可以在这个事件处理函数中执行一些与异步依赖成功加载相关的逻辑。
@fallback
是在fallback
插槽的内容显示时触发。- 当异步依赖没有在规定时间内(如果设置了
timeout
)解析完成或者出现错误时触发。可以在这个事件处理函数中处理错误情况或显示特定的 fallback 内容。
- 当异步依赖没有在规定时间内(如果设置了
错误处理
<Suspense>
组件自身目前还不提供错误处理,可以使用 errorCaptured
选项或者 onErrorCaptured()
钩子,在使用到 <Suspense>
的父组件中捕获和处理异步错误。
errorCaptured
选项
在 Vue 组件的选项中,可以定义errorCaptured
方法。这个方法会在捕获到来自子孙组件(包括异步组件)的错误时被调用。
export default {
errorCaptured(error, component, info) {
// 处理错误的逻辑
console.log('Error captured:', error);
// 可以根据需要返回 false 来阻止错误继续向上传播
return false;
}
}
参数说明:
error
参数是捕获到的错误对象,component
是抛出错误的组件实例,info
是一个包含错误来源信息的字符串。
当在包含<Suspense>
的父组件中定义了errorCaptured
选项时,如果<Suspense>
内部的异步组件或异步操作抛出错误,errorCaptured
方法将被调用。
<template>
<div>
<Suspense>
<template v-slot:default>
<Child />
</template>
<template v-slot:fallback>
<div>Loading...</div>
</template>
</Suspense>
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
errorCaptured(error, component, info) {
console.log('Error in Suspense:', error);
}
});
</script>
<script setup lang="ts">
import { Suspense } from 'vue';
import Child from './Child.vue';
</script>
onErrorCaptured ()
钩子(组合式 API)
在 Vue 3 的组合式 API 中,可以使用onErrorCaptured
函数来注册一个错误捕获钩子。
<script setup>
import { onErrorCaptured } from 'vue';
onErrorCaptured((error, component, info) => {
console.log('Error captured with onErrorCaptured:', error);
});
</script>
当在包含<Suspense>
的父组件中使用onErrorCaptured
钩子时,如果<Suspense>
内部的异步组件或异步操作抛出错误,这个钩子将被调用。
注意:
在errorCaptured
方法或onErrorCaptured
钩子中,可以根据需要返回false
来阻止错误继续向上传播。如果不返回false
或者返回true
,错误将继续向上传递给父组件的错误处理机制。
嵌套使用
当有多个类似于下方的异步组件 (常见于嵌套或基于布局的路由) 时:
<Suspense>
<component :is="DynamicAsyncOuter">
<component :is="DynamicAsyncInner" />
</component>
</Suspense>
<Suspense>
创建了一个边界,它将如预期的那样解析树下的所有异步组件。
- 当更改
DynamicAsyncOuter
时,<Suspense>
会正确地等待它 - 当更改
DynamicAsyncInner
时,嵌套的DynamicAsyncInner
会呈现为一个空节点,直到它被解析为止 (而不是之前的节点或回退插槽)。
可以使用嵌套的方法来解决这个问题:
<Suspense>
<component :is="DynamicAsyncOuter">
<Suspense suspensible> <!-- 像这样 -->
<component :is="DynamicAsyncInner" />
</Suspense>
</component>
</Suspense>
设置 suspensible
属性后,所有异步依赖项处理都会交给父级 <Suspense>
(包括发出的事件),而内部 <Suspense>
仅充当依赖项解析和修补的另一个边界。
如果不设置 suspensible
属性,内部的 <Suspense>
将被父级 <Suspense>
视为同步组件。这意味着它将会有自己的回退插槽,如果两个 Dynamic
组件同时被修改,则当子 <Suspense>
加载其自己的依赖关系树时,可能会出现空节点和多个修补周期。