Suspense 是 Vue3 新增的一个内置组件,用于处理异步组件和异步依赖。本文将深入分析其实现原理,理解 Vue3 是如何优雅地处理异步加载状态的。
1. 组件示例
让我们先看一个典型的 Suspense 使用场景:
// AsyncComp.vue
<template>
<div>
<h2>{{ title }}</h2>
<p>{{ content }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
// 模拟异步数据获取
const loadData = async () => {
await new Promise(r => setTimeout(r, 1000))
return {
title: '异步标题',
content: '异步内容'
}
}
const { title, content } = await loadData()
</script>
// App.vue
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComp />
</template>
<!-- 加载状态 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script setup>
import AsyncComp from './AsyncComp.vue'
</script>
这个示例展示了:
-
一个包含异步 setup 的组件
-
Suspense 的基本用法:
- default 插槽:异步组件
- fallback 插槽:加载状态
-
自动处理异步加载状态
-
支持嵌套的异步依赖
2. 组件数据结构
Suspense 组件的核心数据结构:
// 组件属性
interface SuspenseProps {
onResolve?: () => void; // 异步内容解析完成时触发
onPending?: () => void; // 进入挂起状态时触发
onFallback?: () => void; // 显示 fallback 内容时触发
timeout?: string | number; // 超时时间
suspensible?: boolean; // 是否可以被父 Suspense 捕获
}
// Suspense 边界对象
interface SuspenseBoundary {
vnode: VNode; // Suspense 组件的 vnode
parent: SuspenseBoundary | null; // 父 Suspense 边界
parentComponent: ComponentInternalInstance | null;
isSVG: boolean;
container: RendererElement;
hiddenContainer: RendererElement; // 存储异步内容的容器
anchor: RendererNode | null;
activeBranch: VNode | null; // 当前显示的分支
pendingBranch: VNode | null; // 待处理的异步分支
deps: number; // 待处理的异步依赖数
pendingId: number; // 当前挂起状态的 ID
timeout: number; // 超时时间
isInFallback: boolean; // 是否显示 fallback
isHydrating: boolean; // 是否在 SSR 水合过程中
isUnmounted: boolean; // 是否已卸载
effects: Function[]; // 待执行的副作用函数
resolve(force?: boolean): void; // 解析异步内容
fallback(fallbackVNode: VNode): void; // 显示 fallback
move(...args: any[]): void; // 移动 DOM
next(): RendererNode | null; // 获取下一个锚点
registerDep(...args: any[]): void; // 注册异步依赖
unmount(...args: any[]): void; // 卸载组件
}
// 组件定义
const SuspenseImpl = {
name: "Suspense",
__isSuspense: true,
// 处理 Suspense 组件的更新
process(
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals
) {
// 首次挂载
if (n1 == null) {
mountSuspense(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
rendererInternals
);
} else {
// 更新场景
// 特殊情况处理:
// 如果父 Suspense 还未解析完成,当前 Suspense 也需要更新
// 这种情况下需要跳过当前更新,避免重复挂载内部组件
if (
parentSuspense &&
parentSuspense.deps > 0 &&
!n1.suspense!.isInFallback
) {
// 直接复用之前的状态
n2.suspense = n1.suspense!;
n2.suspense.vnode = n2;
n2.el = n1.el;
return;
}
// 正常更新流程
patchSuspense(
n1,
n2,
container,
anchor,
parentComponent,
namespace,
slotScopeIds,
optimized,
rendererInternals
);
}
},
hydrate: hydrateSuspense,
normalize: normalizeSuspenseChildren,
};
process 函数的主要职责:
-
处理首次挂载:
- 调用 mountSuspense 创建 Suspense 实例
- 初始化异步内容和 fallback
- 建立组件间的关系
-
处理特殊更新:
- 检查父 Suspense 状态
- 避免重复挂载组件
- 复用已有实例状态
-
处理正常更新:
- 调用 patchSuspense 更新内容
- 维护组件状态
- 处理异步依赖变化
-
优化处理:
- 支持 SSR hydration
- 处理嵌套 Suspense
- 维护更新顺序
关键数据结构说明:
-
组件相关:
- SuspenseProps:组件属性定义
- SuspenseImpl:组件实现对象
- __isSuspense:用于标识 Suspense 组件
-
边界管理:
- SuspenseBoundary:管理异步边界
- 父子边界关系维护
- 异步依赖追踪
-
状态控制:
- activeBranch/pendingBranch:管理显示内容
- isInFallback:控制 fallback 显示
- deps:追踪异步依赖数量
-
容器处理:
- container:主容器
- hiddenContainer:隐藏容器
- DOM 移动和定位
3. 实现原理分析
3.1 初始化阶段
当 Suspense 组件被创建时:
// 检查是否是 Suspense 组件
export const isSuspense = (type: any): boolean => type.__isSuspense;
// 为每个挂起分支生成唯一 ID
let suspenseId = 0;
function mountSuspense(
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals
) {
const {
p: patch,
o: { createElement },
} = rendererInternals;
// 创建隐藏容器
const hiddenContainer = createElement("div");
// 规范化 Suspense 插槽内容
normalizeSuspenseChildren(vnode);
// 创建 suspense 边界
const suspense = (vnode.suspense = createSuspenseBoundary(
vnode,
parentSuspense,
parentComponent,
container,
hiddenContainer,
anchor,
namespace,
slotScopeIds,
optimized,
rendererInternals
));
// 挂载异步内容到隐藏容器
patch(
null,
(suspense.pendingBranch = vnode.ssContent!),
hiddenContainer,
null,
parentComponent,
suspense,
namespace,
slotScopeIds
);
// 如果有异步依赖,显示 fallback
if (suspense.deps > 0) {
triggerEvent(vnode, "onPending");
triggerEvent(vnode, "onFallback");
patch(
null,
vnode.ssFallback!,
container,
anchor,
parentComponent,
null,
namespace,
slotScopeIds
);
// 设置 fallback 状态
suspense.isInFallback = true;
}
// 处理超时配置
if (vnode.props?.timeout != null) {
const timeout = toNumber(vnode.props.timeout);
if (timeout > 0) {
setTimeout(() => {
if (suspense.pendingBranch) {
suspense.fallback(vnode.ssFallback!);
}
}, timeout);
}
}
}
// 规范化 Suspense 插槽内容
function normalizeSuspenseChildren(vnode: VNode) {
const { shapeFlag, children } = vnode;
const isSlotChildren = shapeFlag & ShapeFlags.SLOTS_CHILDREN;
// 处理默认插槽内容
vnode.ssContent = normalizeSuspenseSlot(
isSlotChildren ? (children as Slots).default : children
);
// 处理 fallback 插槽内容
vnode.ssFallback = isSlotChildren
? normalizeSuspenseSlot((children as Slots).fallback)
: createVNode(Comment);
}
初始化的主要流程:
-
准备工作:
- 创建隐藏容器
- 规范化插槽内容
- 初始化 suspense 边界
- 设置初始状态
-
内容处理:
- 规范化插槽内容
- 处理异步组件
- 准备 fallback 内容
-
边界设置:
- 建立父子边界关系
- 注册异步依赖
- 配置事件处理
3.2 异步依赖处理
function registerDep(
suspense: SuspenseBoundary,
vnode: VNode,
setupRenderEffect: SetupRenderEffectFn
) {
// 增加依赖计数
const depId = suspense.deps++;
// 注册异步组件的 setup 效果
const asyncDep = vnode.type.__asyncLoader!();
asyncDep.then(() => {
if (!suspense.isUnmounted && !suspense.isHydrating) {
// 重新渲染异步组件
setupRenderEffect(/* ... */);
// 检查是否所有依赖都已解析
if (--suspense.deps === 0) {
suspense.resolve();
}
}
});
}
依赖处理流程:
-
依赖注册:
- 跟踪异步依赖数量
- 处理组件加载状态
- 维护依赖关系
-
异步加载:
- 执行异步加载函数
- 处理加载结果
- 触发相应事件
-
状态更新:
- 更新依赖计数
- 检查依赖状态
- 触发内容切换
3.3 内容切换过程
function resolve(suspense: SuspenseBoundary, force = false) {
const { activeBranch, pendingBranch } = suspense;
if (
pendingBranch &&
(force || !isSameVNodeType(activeBranch!, pendingBranch))
) {
const { anchor } = suspense;
// 卸载当前内容
if (activeBranch) {
unmount(activeBranch, parentComponent, suspense, true);
}
// 移动异步内容到主容器
move(pendingBranch!, container, anchor, MoveType.ENTER);
// 更新状态
suspense.activeBranch = pendingBranch;
suspense.pendingBranch = null;
suspense.isInFallback = false;
// 触发事件
triggerEvent(suspense.vnode, "onResolve");
}
}
切换过程说明:
-
状态检查:
- 验证是否需要切换
- 检查内容是否变化
- 处理强制切换
-
内容切换:
- 卸载当前内容
- 移动新内容
- 更新显示状态
-
收尾处理:
- 更新组件引用
- 清理临时状态
- 触发相关事件
3.4 超时处理
if (props.timeout != null) {
setTimeout(() => {
if (suspense.pendingBranch) {
triggerEvent(vnode, "onFallback");
switchToFallback();
}
}, timeout);
}
function switchToFallback() {
// 显示 fallback 内容
if (!suspense.isInFallback) {
suspense.isInFallback = true;
patch(/* fallback content */);
}
}
超时机制:
-
超时配置:
- 设置超时时间
- 创建超时检查
- 处理超时回调
-
降级处理:
- 检查异步状态
- 切换到 fallback
- 触发超时事件
-
状态恢复:
- 保持异步加载
- 支持延迟恢复
- 维护加载状态
4. 总结
Suspense 组件通过以下机制实现异步内容管理:
-
边界管理:
- 创建异步边界
- 处理嵌套关系
- 管理异步依赖
-
状态控制:
- 追踪异步状态
- 处理加载时机
- 管理显示内容
-
内容切换:
- 平滑过渡
- 状态保持
- 事件通知
-
性能优化:
- 异步加载
- 超时处理
- 资源管理