跳转到内容

Suspense

实验性功能

<Suspense>是一个实验性功能。它可能无法达到稳定状态,并且在其达到稳定状态之前,API可能会发生变化。

<Suspense>是一个内置组件,用于在组件树中协调异步依赖。在等待多个嵌套异步依赖在组件树中解决时,它可以渲染加载状态。

异步依赖

为了解释<Suspense>试图解决的问题以及它与这些异步依赖的交互方式,让我们想象一个如下所示的组件层次结构

<Suspense>
└─ <Dashboard>
   ├─ <Profile>
   │  └─ <FriendStatus> (component with async setup())
   └─ <Content>
      ├─ <ActivityFeed> (async component)
      └─ <Stats> (async component)

在组件树中,有多个嵌套组件,它们的渲染依赖于一些需要首先解决的异步资源。在没有<Suspense>的情况下,每个组件都需要处理自己的加载/错误和加载完成状态。在最坏的情况下,我们可能在页面上看到三个加载指示器,内容在不同时间显示。

<Suspense>组件为我们提供了在等待这些嵌套异步依赖解决时显示顶层加载/错误状态的能力。

<Suspense>可以等待两种类型的异步依赖

  1. 具有异步setup()钩子的组件。这包括使用带有顶级await表达式的<script setup>的组件。

  2. 异步组件.

async setup()

Composition API 组件的 setup() 钩子可以是异步的

js
export default {
  async setup() {
    const res = await fetch(...)
    const posts = await res.json()
    return {
      posts
    }
  }
}

如果使用 <script setup>,顶级 await 表达式的存在会自动使组件成为异步依赖

vue
<script setup>
const res = await fetch(...)
const posts = await res.json()
</script>

<template>
  {{ posts }}
</template>

异步组件

异步组件默认是 "可挂起的"。这意味着如果它在父级链中有一个 <Suspense>,它将被视为该 <Suspense> 的异步依赖。在这种情况下,加载状态将由 <Suspense> 控制,并且将忽略组件自己的加载、错误、延迟和超时选项。

异步组件可以通过在其选项中指定 suspensible: false 来选择退出 Suspense 控制,并让组件始终控制其自己的加载状态。

加载状态

<Suspense> 组件有两个插槽:#default#fallback。这两个插槽都只允许有一个直接子节点。如果可能,将显示默认插槽中的节点。如果不可以,将显示回退插槽中的节点。

template
<Suspense>
  <!-- component with nested async dependencies -->
  <Dashboard />

  <!-- loading state via #fallback slot -->
  <template #fallback>
    Loading...
  </template>
</Suspense>

在初次渲染时,<Suspense> 将在内存中渲染其默认插槽内容。如果在处理过程中遇到任何异步依赖,它将进入一个 挂起 状态。在挂起状态下,将显示回退内容。当所有遇到的异步依赖都已解决,<Suspense> 进入一个 已解决 状态,并显示已解决的默认插槽内容。

如果在初次渲染过程中没有遇到任何异步依赖,<Suspense> 将直接进入已解决状态。

一旦进入已解决状态,<Suspense> 只有在 #default 插槽的根节点被替换时才会回到挂起状态。嵌套在树中的新异步依赖 不会 导致 <Suspense> 回到挂起状态。

当发生回退时,回退内容不会立即显示。相反,<Suspense> 将在等待新内容及其异步依赖解决的同时显示之前的 #default 内容。此行为可以通过 timeout 属性进行配置:<Suspense> 将在 timeout 指定的时间后切换到回退内容。如果 timeout 的值为 0,则当默认内容被替换时,将立即显示回退内容。

事件

<Suspense> 组件发出 3 个事件:pendingresolvefallback。当进入挂起状态时,将触发 pending 事件。当新内容在 default 插槽中完成解决时,将发出 resolve 事件。当显示 fallback 插槽的内容时,将触发 fallback 事件。

可以使用这些事件,例如,在加载新组件时在旧 DOM 前显示加载指示器。

错误处理

<Suspense> 目前不通过组件本身提供错误处理 - 然而,您可以使用 errorCaptured 选项或 onErrorCaptured() 钩子来捕获和处理 <Suspense> 父组件中的异步错误。

与其他组件结合

在组合使用<Transition><KeepAlive>组件时,通常需要使用<Suspense>。这些组件的嵌套顺序对于它们正常工作至关重要。

此外,这些组件通常与Vue Router中的<RouterView>组件一起使用。

以下示例展示了如何嵌套这些组件,以便它们都按预期工作。对于更简单的组合,您可以删除不需要的组件。

template
<RouterView v-slot="{ Component }">
  <template v-if="Component">
    <Transition mode="out-in">
      <KeepAlive>
        <Suspense>
          <!-- main content -->
          <component :is="Component"></component>

          <!-- loading state -->
          <template #fallback>
            Loading...
          </template>
        </Suspense>
      </KeepAlive>
    </Transition>
  </template>
</RouterView>

Vue Router内置了对使用动态导入进行懒加载组件的支持。这些与异步组件不同,目前它们不会触发<Suspense>。然而,它们可以作为异步组件的子组件,并且可以通过常规方式触发<Suspense>

嵌套Suspense

  • 仅在3.3及以上版本支持

当我们有多个异步组件(常见于嵌套或基于布局的路由)时,如下所示:

template
<Suspense>
  <component :is="DynamicAsyncOuter">
    <component :is="DynamicAsyncInner" />
  </component>
</Suspense>

<Suspense>创建了一个边界,将解析树中的所有异步组件,正如预期的那样。然而,当我们更改DynamicAsyncOuter时,<Suspense>正确等待它,但当更改DynamicAsyncInner时,嵌套的DynamicAsyncInner将渲染一个空节点,直到它被解析(而不是之前的节点或回退插槽)。

为了解决这个问题,我们可以使用嵌套的<Suspense>来处理嵌套组件的补丁,如下所示:

template
<Suspense>
  <component :is="DynamicAsyncOuter">
    <Suspense suspensible> <!-- this -->
      <component :is="DynamicAsyncInner" />
    </Suspense>
  </component>
</Suspense>

如果您没有设置suspensible属性,父级<Suspense>会将内部<Suspense>视为同步组件。这意味着它有自己的回退插槽,如果两个Dynamic组件同时更改,在子<Suspense>加载其自己的依赖项树时,可能会有空节点和多个补丁周期,这可能不是所希望的。当设置了该属性后,所有异步依赖项处理都由父级<Suspense>处理(包括发出的事件),而内部<Suspense>仅作为依赖项解析和补丁的另一个边界。


相关

Suspense已加载