Skip to content

6.3 Suspense:优雅地处理组件的异步依赖

Suspense 是 Vue 3 中一个革命性的特性,它为处理异步组件提供了优雅的解决方案。通过创建异步边界,Suspense 能够捕获子组件树中的异步依赖,并在等待期间显示回退内容,从而提供流畅的用户体验。本节将深入分析 Suspense 的源码实现,探索其异步依赖管理的核心机制。

核心概念与设计理念

异步边界的概念

Suspense 组件创建了一个"异步边界",它能够:

  1. 捕获异步依赖:检测子组件树中的 async setup() 函数
  2. 状态管理:维护 pending、resolved、rejected 三种状态
  3. 回退渲染:在异步内容加载期间显示 fallback 内容
  4. 错误处理:与 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)
  }
}

挂载流程解析:

  1. 隐藏容器创建:创建离屏 DOM 容器用于渲染异步内容
  2. 边界创建:初始化 SuspenseBoundary 对象
  3. 异步检测:在隐藏容器中渲染内容,检测异步依赖
  4. 条件渲染:根据是否有异步依赖决定显示内容或回退

异步依赖追踪机制

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()
      }
    })
}

依赖追踪流程:

  1. 依赖计数:增加 deps 计数器
  2. Promise 处理:监听 async setup() 的 Promise
  3. 错误捕获:处理异步操作中的错误
  4. 解析处理:当 Promise 解析时重新渲染组件
  5. 依赖递减:完成后减少依赖计数,检查是否可以解析

状态切换与解析机制

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
}

嵌套处理机制:

  1. 依赖传播:子 Suspense 的异步依赖会传播到父 Suspense
  2. ID 管理:使用 pendingId 确保异步操作的正确性
  3. 效果合并:子 Suspense 的副作用会合并到父级
  4. 级联解析:子 Suspense 解析时会检查父级是否可以解析

错误边界集成

错误处理机制

typescript
// 在 registerDep 中的错误处理
instance
  .asyncDep!.catch(err => {
    handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
  })
  .then(asyncSetupResult => {
    // 处理成功的异步结果
  })

Suspense 与 Vue 的错误处理系统深度集成:

  1. 异步错误捕获:自动捕获 async setup() 中的错误
  2. 错误传播:错误会通过组件树向上传播
  3. onErrorCaptured 集成:与错误边界钩子协同工作
  4. 恢复机制:支持错误恢复和重试

超时处理机制

超时配置与处理

typescript
const timeout = vnode.props ? toNumber(vnode.props.timeout) : undefined

const suspense: SuspenseBoundary = {
  // ...
  timeout: typeof timeout === 'number' ? timeout : -1,
  // ...
}

超时机制确保:

  1. 用户体验:防止无限等待
  2. 资源管理:避免内存泄漏
  3. 降级处理:超时后显示回退内容
  4. 可配置性:支持自定义超时时间

实际应用场景

异步组件加载

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 中提供:

  1. 状态可视化:显示当前 Suspense 状态
  2. 依赖追踪:展示异步依赖的解析状态
  3. 性能分析:分析异步加载的性能指标
  4. 错误追踪:显示异步操作中的错误信息

最佳实践总结

1. 合理使用 Suspense

  • 用于真正的异步边界,避免过度嵌套
  • 结合错误边界提供完整的用户体验
  • 考虑网络状况设置合适的超时时间

2. 回退内容设计

  • 提供有意义的加载状态指示
  • 使用骨架屏提升感知性能
  • 保持回退内容的简洁性

3. 性能考虑

  • 预加载关键异步组件
  • 实施适当的缓存策略
  • 监控异步加载的性能指标

4. 错误处理

  • 提供用户友好的错误信息
  • 实现重试机制
  • 记录异步错误用于分析

Suspense 组件通过精巧的异步边界设计,为 Vue 3 应用提供了强大的异步依赖管理能力。其源码展现了现代前端框架在处理复杂异步场景时的技术深度,为开发者构建高性能、用户友好的应用提供了坚实的技术基础。


微信公众号二维码