vue3源码解析:Suspense组件实现

217 阅读5分钟

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>

这个示例展示了:

  1. 一个包含异步 setup 的组件

  2. Suspense 的基本用法:

    • default 插槽:异步组件
    • fallback 插槽:加载状态
  3. 自动处理异步加载状态

  4. 支持嵌套的异步依赖

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 函数的主要职责:

  1. 处理首次挂载:

    • 调用 mountSuspense 创建 Suspense 实例
    • 初始化异步内容和 fallback
    • 建立组件间的关系
  2. 处理特殊更新:

    • 检查父 Suspense 状态
    • 避免重复挂载组件
    • 复用已有实例状态
  3. 处理正常更新:

    • 调用 patchSuspense 更新内容
    • 维护组件状态
    • 处理异步依赖变化
  4. 优化处理:

    • 支持 SSR hydration
    • 处理嵌套 Suspense
    • 维护更新顺序

关键数据结构说明:

  1. 组件相关:

    • SuspenseProps:组件属性定义
    • SuspenseImpl:组件实现对象
    • __isSuspense:用于标识 Suspense 组件
  2. 边界管理:

    • SuspenseBoundary:管理异步边界
    • 父子边界关系维护
    • 异步依赖追踪
  3. 状态控制:

    • activeBranch/pendingBranch:管理显示内容
    • isInFallback:控制 fallback 显示
    • deps:追踪异步依赖数量
  4. 容器处理:

    • 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);
}

初始化的主要流程:

  1. 准备工作:

    • 创建隐藏容器
    • 规范化插槽内容
    • 初始化 suspense 边界
    • 设置初始状态
  2. 内容处理:

    • 规范化插槽内容
    • 处理异步组件
    • 准备 fallback 内容
  3. 边界设置:

    • 建立父子边界关系
    • 注册异步依赖
    • 配置事件处理

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

依赖处理流程:

  1. 依赖注册:

    • 跟踪异步依赖数量
    • 处理组件加载状态
    • 维护依赖关系
  2. 异步加载:

    • 执行异步加载函数
    • 处理加载结果
    • 触发相应事件
  3. 状态更新:

    • 更新依赖计数
    • 检查依赖状态
    • 触发内容切换

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");
  }
}

切换过程说明:

  1. 状态检查:

    • 验证是否需要切换
    • 检查内容是否变化
    • 处理强制切换
  2. 内容切换:

    • 卸载当前内容
    • 移动新内容
    • 更新显示状态
  3. 收尾处理:

    • 更新组件引用
    • 清理临时状态
    • 触发相关事件

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 */);
  }
}

超时机制:

  1. 超时配置:

    • 设置超时时间
    • 创建超时检查
    • 处理超时回调
  2. 降级处理:

    • 检查异步状态
    • 切换到 fallback
    • 触发超时事件
  3. 状态恢复:

    • 保持异步加载
    • 支持延迟恢复
    • 维护加载状态

4. 总结

Suspense 组件通过以下机制实现异步内容管理:

  1. 边界管理:

    • 创建异步边界
    • 处理嵌套关系
    • 管理异步依赖
  2. 状态控制:

    • 追踪异步状态
    • 处理加载时机
    • 管理显示内容
  3. 内容切换:

    • 平滑过渡
    • 状态保持
    • 事件通知
  4. 性能优化:

    • 异步加载
    • 超时处理
    • 资源管理