Appearance
6.3 Suspense:优雅地处理组件的异步依赖
Suspense 是 Vue 3 中一个革命性的特性,它为处理异步组件提供了优雅的解决方案。通过创建异步边界,Suspense 能够捕获子组件树中的异步依赖,并在等待期间显示回退内容,从而提供流畅的用户体验。本节将深入分析 Suspense 的源码实现,探索其异步依赖管理的核心机制。
核心概念与设计理念
异步边界的概念
Suspense 组件创建了一个"异步边界",它能够:
- 捕获异步依赖:检测子组件树中的 async setup() 函数
- 状态管理:维护 pending、resolved、rejected 三种状态
- 回退渲染:在异步内容加载期间显示 fallback 内容
- 错误处理:与 onErrorCaptured 钩子集成处理异步错误
typescript
// core/packages/runtime-core/src/components/Suspense.ts
export interface SuspenseProps {
onResolve?: () => void
onPending?: () => void
onFallback?: () => void
timeout?: string | number
/**
* Allow suspense to be captured by parent suspense
* @default false
*/
suspensible?: boolean
}SuspenseBoundary 接口设计
typescript
export interface SuspenseBoundary {
vnode: VNode<RendererNode, RendererElement, SuspenseProps>
parent: SuspenseBoundary | null
parentComponent: ComponentInternalInstance | null
namespace: ElementNamespace
container: RendererElement
hiddenContainer: RendererElement
activeBranch: VNode | null
pendingBranch: VNode | null
deps: number
pendingId: number
timeout: number
isInFallback: boolean
isHydrating: boolean
isUnmounted: boolean
effects: Function[]
resolve(force?: boolean, sync?: boolean): void
fallback(fallbackVNode: VNode): void
move(container: RendererElement, anchor: RendererNode | null, type: MoveType): void
next(): RendererNode | null
registerDep(instance: ComponentInternalInstance, setupRenderEffect: SetupRenderEffectFn, optimized: boolean): void
unmount(parentSuspense: SuspenseBoundary | null, doRemove?: boolean): void
}关键属性解析:
- activeBranch: 当前激活的内容分支
- pendingBranch: 等待解析的异步分支
- deps: 未解析的异步依赖计数
- pendingId: 用于标识异步操作的唯一ID
- hiddenContainer: 用于渲染异步内容的离屏容器
核心实现机制
SuspenseImpl 组件实现
typescript
export const SuspenseImpl = {
name: 'Suspense',
__isSuspense: true,
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,
): void {
if (n1 == null) {
mountSuspense(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
rendererInternals,
)
} else {
patchSuspense(
n1,
n2,
container,
anchor,
parentComponent,
namespace,
slotScopeIds,
optimized,
rendererInternals,
)
}
},
hydrate: hydrateSuspense,
normalize: normalizeSuspenseChildren,
}挂载阶段:mountSuspense
typescript
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 边界
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,
)
// 检查是否遇到异步依赖
if (suspense.deps > 0) {
// 存在异步依赖
triggerEvent(vnode, 'onPending')
triggerEvent(vnode, 'onFallback')
// 挂载回退树
patch(
null,
vnode.ssFallback!,
container,
anchor,
parentComponent,
null, // fallback 树不会有 suspense 上下文
namespace,
slotScopeIds,
)
setActiveBranch(suspense, vnode.ssFallback!)
} else {
// Suspense 没有异步依赖,直接解析
suspense.resolve(false, true)
}
}挂载流程解析:
- 隐藏容器创建:创建离屏 DOM 容器用于渲染异步内容
- 边界创建:初始化 SuspenseBoundary 对象
- 异步检测:在隐藏容器中渲染内容,检测异步依赖
- 条件渲染:根据是否有异步依赖决定显示内容或回退
异步依赖追踪机制
registerDep 方法:注册异步依赖
typescript
registerDep(instance, setupRenderEffect, optimized) {
const isInPendingSuspense = !!suspense.pendingBranch
if (isInPendingSuspense) {
suspense.deps++
}
const hydratedEl = instance.vnode.el
instance
.asyncDep!.catch(err => {
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
})
.then(asyncSetupResult => {
// 当 setup() Promise 解析时重试
// 组件可能在解析前已被卸载
if (
instance.isUnmounted ||
suspense.isUnmounted ||
suspense.pendingId !== instance.suspenseId
) {
return
}
// 从此组件重试
instance.asyncResolved = true
const { vnode } = instance
if (__DEV__) {
pushWarningContext(vnode)
}
handleSetupResult(instance, asyncSetupResult, false)
if (hydratedEl) {
// 如果在异步依赖解析前发生了更新,vnode 可能已被替换
vnode.el = hydratedEl
}
const placeholder = !hydratedEl && instance.subTree.el
setupRenderEffect(
instance,
vnode,
parentNode(hydratedEl || instance.subTree.el!)!,
hydratedEl ? null : next(instance.subTree),
suspense,
namespace,
optimized,
)
if (placeholder) {
remove(placeholder)
}
updateHOCHostEl(instance, vnode.el)
if (__DEV__) {
popWarningContext()
}
// 只有在 suspense 尚未解析时才减少依赖计数
if (isInPendingSuspense && --suspense.deps === 0) {
suspense.resolve()
}
})
}依赖追踪流程:
- 依赖计数:增加
deps计数器 - Promise 处理:监听 async setup() 的 Promise
- 错误捕获:处理异步操作中的错误
- 解析处理:当 Promise 解析时重新渲染组件
- 依赖递减:完成后减少依赖计数,检查是否可以解析
状态切换与解析机制
resolve 方法:解析异步边界
typescript
resolve(resume = false, sync = false) {
if (__DEV__) {
if (!resume && !suspense.pendingBranch) {
throw new Error(
`suspense.resolve() is called without a pending branch.`,
)
}
if (suspense.isUnmounted) {
throw new Error(
`suspense.resolve() is called on an already unmounted suspense boundary.`,
)
}
}
const {
vnode,
activeBranch,
pendingBranch,
pendingId,
effects,
parentComponent,
container,
} = suspense
// 如果有过渡动画正在进行,需要等待其完成
let delayEnter: boolean | null = false
if (suspense.isHydrating) {
suspense.isHydrating = false
} else if (!resume) {
delayEnter =
activeBranch &&
pendingBranch!.transition &&
pendingBranch!.transition.mode === 'out-in'
if (delayEnter) {
activeBranch!.transition!.afterLeave = () => {
if (pendingId === suspense.pendingId) {
move(
pendingBranch!,
container,
anchor === initialAnchor ? next(activeBranch!) : anchor,
MoveType.ENTER,
)
queuePostFlushCb(effects)
}
}
}
// 卸载当前激活的树
if (activeBranch) {
if (parentNode(activeBranch.el!) === container) {
anchor = next(activeBranch)
}
unmount(activeBranch, parentComponent, suspense, true)
}
if (!delayEnter) {
// 将内容从离屏容器移动到实际容器
move(pendingBranch!, container, anchor, MoveType.ENTER)
}
}
setActiveBranch(suspense, pendingBranch!)
suspense.pendingBranch = null
suspense.isInFallback = false
// 刷新缓冲的副作用
// 检查是否有待解析的父 suspense
let parent = suspense.parent
let hasUnresolvedAncestor = false
while (parent) {
if (parent.pendingBranch) {
// 找到待解析的父 suspense,将缓冲的后置任务合并到父级
parent.effects.push(...effects)
hasUnresolvedAncestor = true
break
}
parent = parent.parent
}
// 没有待解析的父 suspense 也没有过渡,刷新所有任务
if (!hasUnresolvedAncestor && !delayEnter) {
queuePostFlushCb(effects)
}
suspense.effects = []
// 如果所有异步依赖都已解析,解析父 suspense
if (isSuspensible) {
if (
parentSuspense &&
parentSuspense.pendingBranch &&
parentSuspenseId === parentSuspense.pendingId
) {
parentSuspense.deps--
if (parentSuspense.deps === 0 && !sync) {
parentSuspense.resolve()
}
}
}
// 触发 @resolve 事件
triggerEvent(vnode, 'onResolve')
}fallback 方法:切换到回退状态
typescript
fallback(fallbackVNode) {
if (!suspense.pendingBranch) {
return
}
const { vnode, activeBranch, parentComponent, container, namespace } = suspense
// 触发 @fallback 事件
triggerEvent(vnode, 'onFallback')
const anchor = next(activeBranch!)
const mountFallback = () => {
if (!suspense.isInFallback) {
return
}
// 挂载回退树
patch(
null,
fallbackVNode,
container,
anchor,
parentComponent,
null, // fallback 树不会有 suspense 上下文
namespace,
slotScopeIds,
optimized,
)
setActiveBranch(suspense, fallbackVNode)
}
const delayEnter =
fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
if (delayEnter) {
activeBranch!.transition!.afterLeave = mountFallback
}
suspense.isInFallback = true
// 卸载当前激活分支
unmount(
activeBranch!,
parentComponent,
null, // 没有 suspense 所以卸载钩子现在触发
true, // shouldRemove
)
if (!delayEnter) {
mountFallback()
}
}嵌套 Suspense 处理
父子 Suspense 协调
typescript
function createSuspenseBoundary(
vnode: VNode,
parentSuspense: SuspenseBoundary | null,
parentComponent: ComponentInternalInstance | null,
container: RendererElement,
hiddenContainer: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
rendererInternals: RendererInternals,
isHydrating = false,
): SuspenseBoundary {
// 如果设置了 `suspensible: true`,将当前 suspense 设为父 suspense 的依赖
let parentSuspenseId: number | undefined
const isSuspensible = isVNodeSuspensible(vnode)
if (isSuspensible) {
if (parentSuspense && parentSuspense.pendingBranch) {
parentSuspenseId = parentSuspense.pendingId
parentSuspense.deps++
}
}
const timeout = vnode.props ? toNumber(vnode.props.timeout) : undefined
if (__DEV__) {
assertNumber(timeout, `Suspense timeout`)
}
const initialAnchor = anchor
const suspense: SuspenseBoundary = {
vnode,
parent: parentSuspense,
parentComponent,
namespace,
container,
hiddenContainer,
deps: 0,
pendingId: suspenseId++,
timeout: typeof timeout === 'number' ? timeout : -1,
activeBranch: null,
pendingBranch: null,
isInFallback: !isHydrating,
isHydrating,
isUnmounted: false,
effects: [],
// ... 方法实现
}
return suspense
}嵌套处理机制:
- 依赖传播:子 Suspense 的异步依赖会传播到父 Suspense
- ID 管理:使用
pendingId确保异步操作的正确性 - 效果合并:子 Suspense 的副作用会合并到父级
- 级联解析:子 Suspense 解析时会检查父级是否可以解析
错误边界集成
错误处理机制
typescript
// 在 registerDep 中的错误处理
instance
.asyncDep!.catch(err => {
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
})
.then(asyncSetupResult => {
// 处理成功的异步结果
})Suspense 与 Vue 的错误处理系统深度集成:
- 异步错误捕获:自动捕获 async setup() 中的错误
- 错误传播:错误会通过组件树向上传播
- onErrorCaptured 集成:与错误边界钩子协同工作
- 恢复机制:支持错误恢复和重试
超时处理机制
超时配置与处理
typescript
const timeout = vnode.props ? toNumber(vnode.props.timeout) : undefined
const suspense: SuspenseBoundary = {
// ...
timeout: typeof timeout === 'number' ? timeout : -1,
// ...
}超时机制确保:
- 用户体验:防止无限等待
- 资源管理:避免内存泄漏
- 降级处理:超时后显示回退内容
- 可配置性:支持自定义超时时间
实际应用场景
异步组件加载
vue
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div class="loading">加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() => import('./HeavyComponent.vue'))
</script>数据获取场景
vue
<template>
<Suspense>
<template #default>
<UserProfile :user-id="userId" />
</template>
<template #fallback>
<UserProfileSkeleton />
</template>
</Suspense>
</template>
<script setup>
// UserProfile.vue
const props = defineProps(['userId'])
// async setup() 会被 Suspense 捕获
const userData = await fetchUser(props.userId)
const userPosts = await fetchUserPosts(props.userId)
</script>嵌套 Suspense 应用
vue
<template>
<Suspense>
<template #default>
<div class="app">
<Header />
<Suspense>
<template #default>
<MainContent />
</template>
<template #fallback>
<ContentSkeleton />
</template>
</Suspense>
<Suspense>
<template #default>
<Sidebar />
</template>
<template #fallback>
<SidebarSkeleton />
</template>
</Suspense>
</div>
</template>
<template #fallback>
<AppSkeleton />
</template>
</Suspense>
</template>错误边界结合使用
vue
<template>
<ErrorBoundary>
<Suspense>
<template #default>
<AsyncDataComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
<template #error="{ error, retry }">
<ErrorDisplay :error="error" @retry="retry" />
</template>
</ErrorBoundary>
</template>性能优化策略
1. 智能回退策略
javascript
// 根据网络状况调整超时时间
const getTimeoutByConnection = () => {
const connection = navigator.connection
if (connection) {
switch (connection.effectiveType) {
case 'slow-2g':
case '2g':
return 8000
case '3g':
return 5000
case '4g':
return 3000
default:
return 2000
}
}
return 3000
}2. 预加载优化
javascript
// 预加载关键异步组件
const preloadCriticalComponents = () => {
import('./CriticalComponent.vue')
import('./ImportantFeature.vue')
}
// 在路由变化时预加载
router.beforeEach((to, from, next) => {
if (to.meta.preload) {
preloadCriticalComponents()
}
next()
})3. 缓存策略
javascript
// 缓存异步数据
const dataCache = new Map()
const fetchWithCache = async (key, fetcher) => {
if (dataCache.has(key)) {
return dataCache.get(key)
}
const data = await fetcher()
dataCache.set(key, data)
return data
}调试与开发工具
开发模式增强
typescript
if (__DEV__ && !__TEST__ && !hasWarned) {
hasWarned = true
console[console.info ? 'info' : 'log'](
`<Suspense> is an experimental feature and its API will likely change.`,
)
}Vue DevTools 集成
Suspense 在 Vue DevTools 中提供:
- 状态可视化:显示当前 Suspense 状态
- 依赖追踪:展示异步依赖的解析状态
- 性能分析:分析异步加载的性能指标
- 错误追踪:显示异步操作中的错误信息
最佳实践总结
1. 合理使用 Suspense
- 用于真正的异步边界,避免过度嵌套
- 结合错误边界提供完整的用户体验
- 考虑网络状况设置合适的超时时间
2. 回退内容设计
- 提供有意义的加载状态指示
- 使用骨架屏提升感知性能
- 保持回退内容的简洁性
3. 性能考虑
- 预加载关键异步组件
- 实施适当的缓存策略
- 监控异步加载的性能指标
4. 错误处理
- 提供用户友好的错误信息
- 实现重试机制
- 记录异步错误用于分析
Suspense 组件通过精巧的异步边界设计,为 Vue 3 应用提供了强大的异步依赖管理能力。其源码展现了现代前端框架在处理复杂异步场景时的技术深度,为开发者构建高性能、用户友好的应用提供了坚实的技术基础。
