Suspense

什么是 Suspense?

Suspense 是 Vue3 中的一个内置组件,用于处理异步组件的加载状态。它允许我们在等待异步组件加载完成时显示一个 fallback 内容,从而提供更好的用户体验。

1. 基本用法

Suspense 组件有两个插槽:

  • default:用于放置异步组件
  • fallback:用于放置加载状态的 fallback 内容

示例:基本 Suspense

<template>
  <div class="app">
    <h1>Suspense 示例</h1>
    <button @click="showAsyncComponent = !showAsyncComponent">
      {{ showAsyncComponent ? '隐藏' : '显示' }} 异步组件
    </button>
    
    <Suspense v-if="showAsyncComponent">
      <!-- 默认插槽:异步组件 -->
      <AsyncComponent />
      <!-- fallback 插槽:加载状态 -->
      <template #fallback>
        <div class="loading">
          <div class="spinner"></div>
          <p>加载中...</p>
        </div>
      </template>
    </Suspense>
  </div>
</template>

<script setup>
import { ref } from 'vue';
// 异步组件
const AsyncComponent = () => import('./AsyncComponent.vue');

const showAsyncComponent = ref(false);
</script>

<style scoped>
.app {
  padding: 20px;
}

.loading {
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 20px;
}

.spinner {
  width: 40px;
  height: 40px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #42b883;
  border-radius: 50%;
  animation: spin 1s linear infinite;
  margin-bottom: 10px;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
</style>

AsyncComponent.vue 组件

<template>
  <div class="async-component">
    <h2>异步组件</h2>
    <p>这是一个异步加载的组件</p>
    <p>数据:{{ data }}</p>
  </div>
</template>

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

const data = ref('');

// 模拟异步数据加载
function fetchData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve('异步加载的数据');
    }, 2000);
  });
}

// 使用 await 等待数据加载
const result = await fetchData();
data.value = result;
</script>

<style scoped>
.async-component {
  padding: 20px;
  border: 1px solid #ddd;
  border-radius: 8px;
  background-color: #f9f9f9;
}
</style>

2. 异步组件

Suspense 通常与异步组件一起使用。在 Vue3 中,我们可以使用 defineAsyncComponent 函数创建异步组件,或者直接使用动态导入。

示例:使用 defineAsyncComponent

<template>
  <Suspense>
    <AsyncComponentWithOptions />
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

// 使用 defineAsyncComponent 创建异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
  // 加载函数
  loader: () => import('./AsyncComponent.vue'),
  
  // 加载状态的 fallback 组件
  loadingComponent: LoadingComponent,
  
  // 加载超时时间
  timeout: 3000,
  
  // 加载失败时显示的组件
  errorComponent: ErrorComponent,
  
  // 错误信息传递给 errorComponent 的 props
  errorComponentProps: {
    message: '加载失败,请重试'
  }
});

// 加载组件
const LoadingComponent = {
  template: '<div class="loading">加载中...</div>'
};

// 错误组件
const ErrorComponent = {
  props: ['message'],
  template: '<div class="error">{{ message }}</div>'
};
</script>

3. 多个异步组件

Suspense 可以包含多个异步组件,它会等待所有异步组件加载完成后再显示内容。

示例:多个异步组件

<template>
  <Suspense>
    <div class="async-components">
      <AsyncComponent1 />
      <AsyncComponent2 />
      <AsyncComponent3 />
    </div>
    <template #fallback>
      <div class="loading">加载中...</div>
    </template>
  </Suspense>
</template>

<script setup>
import { defineAsyncComponent } from 'vue';

const AsyncComponent1 = defineAsyncComponent(() => import('./AsyncComponent1.vue'));
const AsyncComponent2 = defineAsyncComponent(() => import('./AsyncComponent2.vue'));
const AsyncComponent3 = defineAsyncComponent(() => import('./AsyncComponent3.vue'));
</script>

4. 错误处理

当异步组件加载失败时,我们可以使用 onErrorCaptured 钩子来处理错误,或者使用 errorCaptured 选项。

示例:错误处理

<template>
  <div>
    <Suspense>
      <AsyncComponentWithError />
      <template #fallback>
        <div class="loading">加载中...</div>
      </template>
    </Suspense>
    
    <div v-if="error" class="error">
      <p>加载失败:{{ error.message }}</p>
      <button @click="reload">重试</button>
    </div>
  </div>
</template>

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

const error = ref(null);
let AsyncComponentWithError;

// 初始加载
loadComponent();

function loadComponent() {
  error.value = null;
  AsyncComponentWithError = () => import('./AsyncComponentWithError.vue');
}

function reload() {
  loadComponent();
}

// 捕获错误
onErrorCaptured((err) => {
  error.value = err;
  return true; // 阻止错误继续传播
});
</script>

AsyncComponentWithError.vue

<template>
  <div>这是一个会出错的异步组件</div>
</template>

<script setup>
// 模拟加载错误
throw new Error('组件加载失败');
</script>

5. Suspense 的使用场景

  • 异步组件加载
  • 异步数据获取
  • 动态组件切换
  • 路由懒加载
  • 大型组件的按需加载

6. Suspense 的最佳实践

  • 为 Suspense 提供有意义的 fallback 内容,如加载指示器或骨架屏
  • 设置合理的加载超时时间,避免用户等待过长时间
  • 提供错误处理机制,当加载失败时显示友好的错误信息
  • 考虑使用骨架屏(Skeleton Screen)提升用户体验
  • 避免在 Suspense 中嵌套过多的异步组件,可能会导致加载时间过长
  • 结合路由懒加载使用,提高应用的初始加载性能

7. Suspense 与其他 API 的结合

与 Teleport 结合

Suspense 可以与 Teleport 结合使用,实现异步组件的跨 DOM 树渲染。

示例:Suspense 与 Teleport

<template>
  <Teleport to="body">
    <Suspense>
      <AsyncModal />
      <template #fallback>
        <div class="modal-overlay">
          <div class="loading">加载中...</div>
        </div>
      </template>
    </Suspense>
  </Teleport>
</template>

与 KeepAlive 结合

Suspense 可以与 KeepAlive 结合使用,实现异步组件的缓存。

示例:Suspense 与 KeepAlive

<template>
  <KeepAlive>
    <Suspense>
      <AsyncComponent />
      <template #fallback>
        <div class="loading">加载中...</div>
      </template>
    </Suspense>
  </KeepAlive>
</template>

8. Suspense 的限制

  • Suspense 只能用于异步组件或包含异步 setup() 函数的组件
  • Suspense 不支持服务器端渲染(SSR)
  • Suspense 的错误处理需要使用 onErrorCaptured 钩子
  • Suspense 不能嵌套过深,否则会影响性能