说透 Suspense 组件的实现原理

从整体上理解 Suspense 组件源码实现

与其他内置组件一样,Suspense 组件的实现也依赖于渲染器的底层支持。但是为了避免渲染器逻辑代码"膨胀",当用户没有使用 Suspense 组件时,可以利用 TreeShaking 机制在最终的 bundle 中删除 Suspense 相关的代码,使得最终构建包的体积变小 ,Vue 已将 Suspense 组件的渲染逻辑从渲染器中分离出来。

Tips:本文中的源码均摘自 Vue.js 3.5.5,为了方便理解,会省略与本文主题无关的代码。

Suspense 组件其实就是对象,该对象包含 2 个属性,3 个方法:

  • name 属性很简单,就是指定组件的名字为 Suspense

  • __isSuspense 属性值为 true ,是 Suspense 组件独有标识,可通过该属性来判断组件是否为 Suspense 组件。

ts 复制代码
// core/packages/runtime-core/src/components/Suspense.ts

export const isSuspense = (type: any): boolean => type.__isSuspense
  • process 方法负责组件的创建和更新逻辑
  • hydrate 方法负责同构渲染过程中的客户端激活
  • normalize 方法负责规范化 Suspense 组件的子节点

process 方法分析

process 方法负责组件的创建和更新逻辑。我们看看 process 方法的签名:

ts 复制代码
// core/packages/runtime-core/src/components/Suspense.ts

process(
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
  // platform-specific impl passed from renderer
  rendererInternals: RendererInternals,
): void {
  // ...
}

为了使 Suspense 组件支持摇树优化(tree shaking),需要避免直接在渲染器中导入 Suspense 组件。渲染器会检查虚拟节点(vnode)的类型(type)上是否有 __isSuspense 标志 ,并调用此类型(type)的 process 方法,同时传递渲染器内部对象(rendererInternals)到 process 方法内,这样可以在 process 方法内部调用渲染器中的方法,在 process 方法内部实现 Suspense 组件的挂载与更新。

判断虚拟节点(vnode)的类型(type)上是否有 __isSuspense 标志来判断是否为 Suspense 组件:

ts 复制代码
// core/packages/runtime-core/src/vnode.ts

const shapeFlag = isString(type)
  ? ShapeFlags.ELEMENT
  : __FEATURE_SUSPENSE__ && isSuspense(type)
    ? ShapeFlags.SUSPENSE
    : isTeleport(type)
      ? ShapeFlags.TELEPORT
      : isObject(type)
        ? ShapeFlags.STATEFUL_COMPONENT
        : isFunction(type)
          ? ShapeFlags.FUNCTIONAL_COMPONENT
          : 0
  • n1 旧的虚拟节点(vnode)
  • n2 新的虚拟节点(vnode)
  • container 渲染的目标容器
  • anchor 渲染的锚点
  • parentComponent 父组件的实例
  • parentSuspenseSuspense 边界
  • namespace 元素的命名空间
  • slotScopeIds 插槽作用域 ID
  • optimized 是否进行优化
  • rendererInternals 渲染器传入的内部对象

当旧的虚拟节点 n1 不存在时,走创建逻辑,否则走更新逻辑:

ts 复制代码
// core/packages/runtime-core/src/components/Suspense.ts

process(
  n1: VNode | null,
  n2: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
  // platform-specific impl passed from renderer
  rendererInternals: RendererInternals,
): void {
  if (n1 == null) {
    // 创建逻辑
  } else {
    // 更新逻辑
  }
}

创建节点

创建 Suspense 节点的是 mountSuspense 方法

ts 复制代码
// core/packages/runtime-core/src/components/Suspense.ts

if (n1 == null) {
  mountSuspense(
    n2,
    container,
    anchor,
    parentComponent,
    parentSuspense,
    namespace,
    slotScopeIds,
    optimized,
    rendererInternals,
  )
}

接下来具体看看 mountSuspense 方法的实现。

在创建 Suspense 节点时,依赖了一些渲染器的方法

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const {
  p: patch,
  o: { createElement },
} = rendererInternals
  • patch 渲染器中挂载与更新组件的方法
  • createElement 渲染器中创建元素的方法

在浏览器中,createElement 可以简单的理解为 document.createElement 方法,即创建 html 元素的方法。

创建隐藏容器:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const hiddenContainer = createElement('div')

createSuspenseBoundary 方法创建的对象 suspense 会作为 Suspense 组件的上下文对象。该上下文对象会保存到虚拟 dom (vnode)的 suspense 属性中。该上下文对象记录了当前这个 Suspense 组件的所有信息,例如:是否处于 fallback 阶段、是服务端渲染吗等信息,还有一些操作 Suspense 组件的方法。后面对 Suspense 组件的一系列操作都是以这个对象为中心的。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const suspense = (vnode.suspense = createSuspenseBoundary(
  vnode,
  parentSuspense,
  parentComponent,
  container,
  hiddenContainer,
  anchor,
  namespace,
  slotScopeIds,
  optimized,
  rendererInternals,
))

虚拟节点中 ssContent 是 Suspense 组件默认插槽的内容,即 Suspense 组件最终要渲染的内容。

先将 Suspense 组件默认插槽 ssContent 挂载到隐藏容器(hiddenContainer)中,先不展示出来:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

patch(
  null,
  (suspense.pendingBranch = vnode.ssContent!),
  hiddenContainer, // 隐藏容器
  null,
  parentComponent,
  suspense,
  namespace,
  slotScopeIds,
)

通过判断 Suspense 组件的上下文对象 suspensedeps 属性是否大于 0 确定是否存在异步依赖。

存在异步依赖,则会触发 onPendingonFallback 事件,将 ssFallback 内容渲染到页面。

ssFallback 是后备插槽中的内容,此时页面上就会显示出后备插槽的内容,比如提示正在加载中等。

然后将 ssFallback 后备插槽的虚拟节点(vnode)设置为当前激活分支(activeBranch

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.deps > 0) {
  // has async
  // invoke @fallback event
  triggerEvent(vnode, 'onPending')
  triggerEvent(vnode, 'onFallback')

  // mount the fallback tree
  patch(
    null,
    vnode.ssFallback!,
    container,
    anchor,
    parentComponent,
    null, // fallback tree will not have suspense context
    namespace,
    slotScopeIds,
  )
  setActiveBranch(suspense, vnode.ssFallback!)
}

setActiveBranch 函数用于设置当前激活分支。

activeBranch:激活分支,已经挂载到页面中的vnode,在更新的时候会被卸载,且重新被赋值为新挂载的 vnode

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

function setActiveBranch(suspense: SuspenseBoundary, branch: VNode) {
  suspense.activeBranch = branch
  const { vnode, parentComponent } = suspense
  let el = branch.el
  // if branch has no el after patch, it's a HOC wrapping async components
  // drill and locate the placeholder comment node
  // 如果 branch 更新之后没有 el 属性,则说明异步组件或 async setup 还没有解析完成,
  // 此时需要进一步循环遍历 branch 的 el 属性,找到占位的注释节点。
  while (!el && branch.component) {
    branch = branch.component.subTree
    el = branch.el
  }
  vnode.el = el
  // in case suspense is the root node of a component,
  // recursively update the HOC el
  if (parentComponent && parentComponent.subTree === vnode) {
    parentComponent.vnode.el = el
    updateHOCHostEl(parentComponent, el)
  }
}

在更新 Suspense 组件的时候,如果异步组件或 async setup() 没有解析完成,则 Suspense 组件的默认插槽中会渲染空的注释节点,该注释节点仅用于占位。

最后在渲染器中执行更新的时候,会通过该 el 拿对应的父级元素,如果 el 为 null ,则会报错:

具体可见这个 issue :Crash on 'update' non-resolved async component

不过 Vue 官方在 3.4.8 版本中修复这个 bug ,在检查到 el 为 null 时,会进一步循环遍历 branch 的子组件,直到找到非空的 el 为止:

👆 源码链接:github.com/vuejs/core/...

updateHOCHostEl 函数的作用是用于递归更新高阶组件(HOC)的父子关系中的组件节点的宿主元素,确保高阶组件的宿主元素正确映射和更新。这个函数在处理嵌套组件结构时起到了维护和更新宿主元素的作用。

ts 复制代码
// packages/runtime-core/src/componentRenderUtils.ts

export function updateHOCHostEl(
  { vnode, parent }: ComponentInternalInstance,
  el: typeof vnode.el, // HostNode
): void {
  while (parent) {
    const root = parent.subTree
    if (root.suspense && root.suspense.activeBranch === vnode) {
      root.el = vnode.el
    }
    // 父组件的 subTree 指向子组件的 vnode ,
    // 说明这是一个高阶组件的情况
    if (root === vnode) {      
      ;(vnode = parent.vnode).el = el
      parent = parent.parent
    } else {
      break
    }
  }
}

createSuspenseBoundary 函数分析

接下来详细分析一下 createSuspenseBoundary 函数,createSuspenseBoundary 函数最终返回一个对象,该对象可以称为 Suspense 边界或者是 Suspense 的上下文对象。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

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 {
  /* v8 ignore start */
  if (__DEV__ && !__TEST__ && !hasWarned) {
    hasWarned = true
    // @ts-expect-error `console.info` cannot be null error
    // eslint-disable-next-line no-console
    console[console.info ? 'info' : 'log'](
      `<Suspense> is an experimental feature and its API will likely change.`,
    )
  }
  /* v8 ignore stop */

  // ...

  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: [],

    resolve(resume = false, sync = false) {
      // ...
    },

    fallback(fallbackVNode) {
      // ...
    },

    move(container, anchor, type) {
      // ...
    },

    next() {
      return suspense.activeBranch && next(suspense.activeBranch)
    },

    registerDep(instance, setupRenderEffect, optimized) {
      // ...
    },

    unmount(parentSuspense, doRemove) {
      // ...
    },
  }

  return suspense
}

/* v8 ignore start *//* v8 ignore stop */ 注释用于忽略测试覆盖率计算

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

/* v8 ignore start */
if (__DEV__ && !__TEST__ && !hasWarned) {
  hasWarned = true
  // @ts-expect-error `console.info` cannot be null error
  // eslint-disable-next-line no-console
  console[console.info ? 'info' : 'log'](
    `<Suspense> is an experimental feature and its API will likely change.`,
  )
}
/* v8 ignore stop */

从渲染器传入的对象中取出相关方法以备后续使用:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const {
  p: patch,
  m: move,
  um: unmount,
  n: next,
  o: { parentNode, remove },
} = rendererInternals

如果用户传入了 suspensible 为 true 的 prop ,则将当前组件的异步依赖作为父级 Suspense 的依赖项。

使用 isVNodeSuspensible() 函数判断用户是否设置了 suspensible 为 true 。如果返回的变量 isSuspensible 为 true ,则将父级的 Suspense 组件的 pendingId 赋值给 parentSuspenseId ,并增加父级 Suspense 组件的依赖计数(deps):

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// if set `suspensible: true`, set the current suspense as a dep of parent suspense
let parentSuspenseId: number | undefined
const isSuspensible = isVNodeSuspensible(vnode)
if (isSuspensible) {
  if (parentSuspense && parentSuspense.pendingBranch) {
    parentSuspenseId = parentSuspense.pendingId
    parentSuspense.deps++
  }
}

如果传入的虚拟 DOM 节点的 suspensible 不为空且不是 false ,则说明 suspensible 为 true :

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

function isVNodeSuspensible(vnode: VNode) {
  const suspensible = vnode.props && vnode.props.suspensible
  return suspensible != null && suspensible !== false
}

获取用户传入的 timeout prop ,并做数字转换:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

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

toNumber() 函数是 Vue.js 源码中公共方法,用于将数字类型的字符串转为数字,对于数字 或者无法转换为数字的字符串则原样返回:

ts 复制代码
// packages/shared/src/general.ts

/**
 * Only concerns number-like strings
 * "123-foo" will be returned as-is
 */
export const toNumber = (val: any): any => {
  const n = isString(val) ? Number(val) : NaN
  return isNaN(n) ? val : n
}

记录锚点元素,后续移动挂起分支(pendingBranch)时使用:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const initialAnchor = anchor

接下来,构造 Suspense 组件的上下文对象:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

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: []
  // ...
}
  • vnode ,当前 Suspense 组件的虚拟 DOM

  • parent,父级 Suspense 组件的上下文对象

  • parentComponent,父级组件的实例对象

  • namespace,表示元素的命名空间。在 HTML 中,命名空间可以用于区分不同的 XML 命名空间或特定的 HTML 版本(如 SVG 的命名空间)

  • container,代表当前 Suspense 上下文对象关联的容器元素

  • hiddenContainer,用于挂载未解析完成的默认插槽中的内容

  • deps,异步依赖的数量

  • pendingId,标识异步依赖(默认插槽中的内容)的唯一标识,用于区分不同的异步依赖

  • timeout,解析异步依赖的超时时间,表示等待多长时间后展示后备内容,若 timeout 为 0 ,则 Suspense 组件回退到挂起状态后立马展示后备内容

  • activeBranch,激活分支,已经挂载到页面中的虚拟 DOM ,在更新的时候会被卸载,且重新被赋值为新挂载的虚拟 DOM

  • pendingBranch,挂起分支,指默认插槽(#default)中的内容。因为存在异步依赖,在异步依赖处理完毕之后,将 activeBranch 卸载完毕后,进入挂载

  • isInFallback,表示当前是否处于后备状态(即等待异步依赖加载期间显示的状态)。如果为 true,表示正在显示后备内容

  • isHydrating,表示当前是否处于水合状态。水合状态是在客户端渲染过程中,将服务器端渲染的内容与客户端的状态同步的过程

  • isUnmounted,表示当前 Suspense 组件是否已卸载

  • effects,存储 Suspense 组件中的副作用函数的数组,这些副作用可能是在渲染过程中需要执行的,比如数据获取、事件监听等

解释完 Suspense 组件上下文对象(SuspenseBoundary)中的属性后,来看看 Suspense 组件上下文对象中的方法:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const suspense: SuspenseBoundary = {
  // ...
  resolve(resume = false, sync = false) {
    // 异步依赖解析完成后调用的方法
  },
  fallback(fallbackVNode) {
    // 用于展示后备插槽中的内容
  },
  move(container, anchor, type) {
    // 移动 activeBranch 内容到指定容器
  },
  next() {
    // 返回 activeBranch 的相邻兄弟元素
  },
  registerDep(instance, setupRenderEffect, optimized) {
    // 注册异步依赖,异步依赖增加则 deps 计数相应增加,
    // 异步依赖解析完成,deps 计数相应减少
  },
  unmount(parentSuspense, doRemove) {
    // 卸载 activeBranch 或 pendingBranch 的内容
  }
}

接下来我们来看看各个方法的具体实现。

resolve 方法

首先来看 resolve 方法的实现,resolve 方法是异步依赖解析完成后调用的方法:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const suspense: SuspenseBoundary = {
  // ...
  resolve(resume = false, sync = false) {
    // ...
  }
}
  • resume ,如果为 true ,强制进行更新

  • sync,如果为 true ,同步进行状态更新

如果在开发环境,在解析前先进行一些前提条件的判断,如果不符合这些条件判断,则抛出异常:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

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.`,
      )
    }
  }
  // ...
}

从 Suspense 组件上下文对象中取出一系列数据方便后面使用:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const {
  vnode,
  // activeBranch 是后备插槽(#fallback)中的内容
  activeBranch,
  // pendingBranch 是默认插槽(#default)中的内容
  pendingBranch,
  pendingId,
  effects,
  parentComponent,
  container,
} = suspense

下面就是一个挂载默认插槽(#default)的操作,主要针对的是客户端渲染,处理离开和进入过渡,在离开过渡完成后,将解析好的异步依赖,即默认插槽中的内容移动到指定容器,完成默认插槽内容的挂载。

如果有过渡,我们需要等待过渡完成再将默认插槽的内容移动到页面容器中:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// if there's a transition happening we need to wait it to finish.
let delayEnter: boolean | null = false

如果是服务端渲染,不进行过渡的处理,因为过渡动效在客户端环境下才能执行:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.isHydrating) {
  suspense.isHydrating = false
}

resume 为 false 表示不强制进行更新。如果 pendingBranch ,即异步依赖解析后的内容存在进入过渡,则 activeBranch ,后备插槽的内容就会有一个离开过渡,此时需要等待离开过渡完成后,再将 pendingBranch 移动到指定容器中:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.isHydrating) {
  suspense.isHydrating = false
} else if (!resume) {
  // 非强制更新
  // pendingBranch 存在进入过渡,则 activeBranch 有离开过渡  
  delayEnter =
    activeBranch &&
    pendingBranch!.transition &&
    pendingBranch!.transition.mode === 'out-in'
  if (delayEnter) {
    // 构造 Transition 组件离开过渡函数
    activeBranch!.transition!.afterLeave = () => {
      if (pendingId === suspense.pendingId) {
        // 等离开过渡执行完成后,再将 pendingBranch 移动到指定容器中
        move(
          pendingBranch!,
          container,
          anchor === initialAnchor ? next(activeBranch!) : anchor,
          MoveType.ENTER,
        )
        // 执行 Suspense 组件的副作用函数
        queuePostFlushCb(effects)
      }
    }
  }
  // ...
}

afterLeave 是 Transition 组件相关的事件,会在离开过渡完成、且元素已从 DOM 中移除时调用
out-in 是 Transition 组件的模式,表示先执行离开动画,后执行进入动画

Suspense 从 fallback 状态切换到主内容时,安全卸载当前显示的 fallback 内容并更新 DOM 锚点。

父级 Suspense 可能移动了当前 fallback 的 DOM 位置,需要获取最新的锚点位置保证新内容插入正确位置。

当使用 out-in 过渡模式时(delayEnter=true),过渡期间可能发生分支切换。导致 activeBranchpendingBranch 同时存在于 hiddenContainer ,此时 next(activeBranch) 可能错误返回 pendingBranch.el ,从而意外地将 pendingBranch 卸载掉了。

在不需要延迟插入的情况下,立即将异步加载完成的内容移动到实际容器中显示。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.isHydrating) {
  suspense.isHydrating = false
} else if (!resume) {
  // ...
  // unmount current active tree
  if (activeBranch) {
    // if the fallback tree was mounted, it may have been moved
    // as part of a parent suspense. get the latest anchor for insertion
    // #8105 if `delayEnter` is true, it means that the mounting of
    // `activeBranch` will be delayed. if the branch switches before
    // transition completes, both `activeBranch` and `pendingBranch` may
    // coexist in the `hiddenContainer`. This could result in
    // `next(activeBranch!)` obtaining an incorrect anchor
    // (got `pendingBranch.el`).
    // Therefore, after the mounting of activeBranch is completed,
    // it is necessary to get the latest anchor.
    // activeBranch 是否在原始容器 ?
    if (parentNode(activeBranch.el!) === container) {
      // 获取 activeBranch 的下一个节点作为锚点
      anchor = next(activeBranch)
    }
    // 卸载当前内容
    unmount(activeBranch, parentComponent, suspense, true)
  }
  if (!delayEnter) {
    // move content from off-dom container to actual container
    // 在不需要延迟插入的情况下,立即将异步加载完成的内容移动到实际容器中显示
    move(pendingBranch!, container, anchor, MoveType.ENTER)
  }
}

完成 Suspense 边界(Suspense 组件上下文对象)的状态转换并处理相关副作用。

将加载完成的内容分支(pendingBranch)设置为当前活动分支,清空待处理分支指针,标记 Suspense 不再处于 fallback 状态:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

setActiveBranch(suspense, pendingBranch!)
suspense.pendingBranch = null
suspense.isInFallback = false

向上遍历父级 Suspense 边界,查找 pending 状态的祖先,如果有 pending 状态的祖先 Suspense ,将当前副作用合并到祖先的队列中,否则且没有延迟进入时,异步执行副作用函数,然后清空当前 Suspense 的副作用队列:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// flush buffered effects
// check if there is a pending parent suspense
let parent = suspense.parent
let hasUnresolvedAncestor = false
while (parent) {
  if (parent.pendingBranch) {
    // found a pending parent suspense, merge buffered post jobs
    // into that parent
    parent.effects.push(...effects)
    hasUnresolvedAncestor = true
    break
  }
  parent = parent.parent
}
// no pending parent suspense nor transition, flush all jobs
if (!hasUnresolvedAncestor && !delayEnter) {
  queuePostFlushCb(effects)
}
suspense.effects = []

如果当前 Suspense 可被父级捕获(suspensible),减少父级 Suspense 的依赖计数,当父级依赖降为 0 时,调用父级 resolve 方法

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// resolve parent suspense if all async deps are resolved
if (isSuspensible) {
  if (
    parentSuspense &&
    parentSuspense.pendingBranch &&
    parentSuspenseId === parentSuspense.pendingId
  ) {
    parentSuspense.deps--
    if (parentSuspense.deps === 0 && !sync) {
      parentSuspense.resolve()
    }
  }
}

调用用户定义的 onResolve 函数:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// invoke @resolve event
triggerEvent(vnode, 'onResolve')
fallback 方法

检查 pendingBranch 是否存在(即是否有异步加载中的内容分支),如果没有待处理的异步分支,则直接退出方法(避免不必要的 fallback 操作)

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (!suspense.pendingBranch) {
  return
}

从 Suspense 边界对象( Suspense 上下文对象)中提取后续操作必需的5个关键属性:

  • vnode:当前 Suspense 组件对应的虚拟节点
  • activeBranch:当前正在显示的内容分支(可能是主内容或前一个 fallback )
  • parentComponent:父组件实例(用于组件上下文传递)
  • container:DOM 容器元素(用于挂载 fallback 内容)
  • namespace:元素命名空间(处理 SVG 等特殊命名空间场景)
ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const { vnode, activeBranch, parentComponent, container, namespace } =
  suspense

调用用户定义的 onFallback 回调:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// invoke @fallback event
triggerEvent(vnode, 'onFallback')

获取当前活动分支的下一个 DOM 节点作为锚点,定义挂载函数(延迟执行或立即执行)

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const anchor = next(activeBranch!)

const mountFallback = () => {
  if (!suspense.isInFallback) {
    return
  }
  // mount the fallback tree
  patch(
    null,
    fallbackVNode,
    container,
    anchor,
    parentComponent,
    null, // fallback tree will not have suspense context
    namespace,
    slotScopeIds,
    optimized,
  )
  setActiveBranch(suspense, fallbackVNode)
}

检查 fallback 内容是否有 out-in 过渡模式,若有,延迟挂载直到当前内容离开动画完成:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const delayEnter =
  fallbackVNode.transition && fallbackVNode.transition.mode === 'out-in'
if (delayEnter) {
  activeBranch!.transition!.afterLeave = mountFallback
}

标记 Suspense 进入 fallback 状态,卸载当前显示的内容:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

suspense.isInFallback = true

// unmount current active branch
unmount(
  activeBranch!,
  parentComponent,
  null, // no suspense so unmount hooks fire now
  true, // shouldRemove
)

若无过渡动画要求,立即挂载 fallback 内容:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (!delayEnter) {
  mountFallback()
}
move 方法

当 Suspense 有当前显示的激活分支(activeBranch)时,调用渲染器内部的 move 方法(来自 rendererInternals),将激活分支对应的 DOM 移动到传入的容器(container)的指定锚点位置(anchor):

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

move(container, anchor, type) {
  suspense.activeBranch &&
    move(suspense.activeBranch, container, anchor, type)
  suspense.container = container
}

该移动方法会提供给渲染器调用:

ts 复制代码
// packages/runtime-core/src/renderer.ts

const move: MoveFn = (
  vnode,
  container,
  anchor,
  moveType,
  parentSuspense = null,
) => {
  if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
    vnode.suspense!.move(container, anchor, moveType)
    return
  }
}
next 方法

获取当前激活分支(activeBranch)对应的 DOM 节点的下一个相邻节点:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

next() {
  return suspense.activeBranch && next(suspense.activeBranch)
}
registerDep 方法

当 Suspense 处于等待状态时(pendingBranch 存在),增加依赖计数 deps++

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const isInPendingSuspense = !!suspense.pendingBranch
if (isInPendingSuspense) {
  suspense.deps++
}

异步组件返回的结果是一个 PromisePromise 可以由 then 回调中得到解析结果。在 catch 回调中捕获异步组件加载过程中的错误:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

instance
  .asyncDep!.catch(err => {
    handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
  })
  .then(asyncSetupResult => {
    // ...
  })

不知道异步组件返回的 Promise 什么时候返回结果,但是在返回的时候可能组件已经被卸载,如果已经被卸载或者是 Suspense 被卸载以及如果不是等待的异步和处理的异步不是同一个,直接结束当前的 Promise 解析:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// retry when the setup() promise resolves.
// component may have been unmounted before resolve.
if (
  instance.isUnmounted ||
  suspense.isUnmounted ||
  suspense.pendingId !== instance.suspenseId
) {
  return
}

设置组件实例的 asyncResolved 标志,表示该异步组件的 setup 函数已完成解析:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

instance.asyncResolved = true

处理异步 setup 函数的返回结果:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

handleSetupResult(instance, asyncSetupResult, false)

恢复原始 DOM 引用。在异步组件加载期间,组件可能因状态更新被重新渲染,重新渲染会创建新的 vnode,其 vnode.el 会被更新为客户端临时节点(通常是注释占位符)这会导致原始服务器渲染的 DOM 引用丢失。

因此在异步组件结果返回前,将 DOM 元素先保存到 hydratedEl 中。

在异步组件结果返回后,再将原始的 DOM 元素重置回 vnode.el 中。确保客户端激活(hydration)时能正确匹配服务器渲染的 DOM 结构:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (hydratedEl) {
  // vnode may have been replaced if an update happened before the
  // async dep is resolved.
  vnode.el = hydratedEl
}

在纯客户端渲染(CSR)场景中,异步组件加载期间会生成注释占位符节点(即 <!---->):

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const placeholder = !hydratedEl && instance.subTree.el

setupRenderEffect 的作用是创建和管理组件的响应式渲染/更新逻辑,在 Suspense 场景中,它确保了异步组件解析后能正确挂载到 DOM 并保持响应式更新能力:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

setupRenderEffect(
  instance,
  vnode,
  // component may have been moved before resolve.
  // if this is not a hydration, instance.subTree will be the comment
  // placeholder.
  parentNode(hydratedEl || instance.subTree.el!)!,
  // anchor will not be used if this is hydration, so only need to
  // consider the comment placeholder case.
  hydratedEl ? null : next(instance.subTree),
  suspense,
  namespace,
  optimized,
)

移除客户端渲染时生成的注释占位节点:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (placeholder) {
  remove(placeholder)
}

当异步组件被高阶组件(HOC)包裹时,需要确保 HOC 的根 DOM 元素引用 (vnode.el) 能正确指向最终渲染的真实 DOM 元素,避免因异步加载导致元素引用错位:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

updateHOCHostEl(instance, vnode.el)

isInPendingSuspense 标识当前是否处于 Suspense 的 pending 状态。

suspense.deps 为 Suspense 边界(Suspense 上下文对象)内未完成的异步依赖计数。

Suspense 的异步任务中嵌套了其他异步任务,每减少一个 deps 代表一个异步任务结束。只有到了最后一个异步任务结束,Suspense 才会进入 resolve 状态:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// only decrease deps count if suspense is not already resolved
if (isInPendingSuspense && --suspense.deps === 0) {
  suspense.resolve()
}
unmount 方法

标记 Suspense 组件为已卸载状态,卸载 Suspense 组件激活分支或 fallback 分支:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

unmount(parentSuspense, doRemove) {
  suspense.isUnmounted = true
  if (suspense.activeBranch) {
    unmount(
      suspense.activeBranch,
      parentComponent,
      parentSuspense,
      doRemove,
    )
  }
  if (suspense.pendingBranch) {
    unmount(
      suspense.pendingBranch,
      parentComponent,
      parentSuspense,
      doRemove,
    )
  }
}

完成挂载

挂载默认插槽的内容到离屏容器中,在离屏容器中初始化异步内容:

  • pendingBranch:存储默认插槽内容(需异步加载的组件)

  • hiddenContainer: 通过 createElement('div') 创建的离屏容器

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

patch(
  null,
  (suspense.pendingBranch = vnode.ssContent!), // 设置 pendingBranch 为默认插槽内容
  hiddenContainer, // 离屏容器(不在 DOM 树中)
  null,
  parentComponent,
  suspense,
  namespace,
  slotScopeIds,
)

检查异步依赖状态,

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.deps > 0) { 
  // 处理异步场景
} else {
  // Suspense 没有异步依赖,则直接调用 resolve 方法
  suspense.resolve(false, true)
}

触发 onPending 、onFallback 回调函数:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.deps > 0) {
  // has async
  // invoke @fallback event
  triggerEvent(vnode, 'onPending')
  triggerEvent(vnode, 'onFallback')

  // ...
}

挂载 fallback 插槽内容,将 fallback 内容设置为当前活动分支:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.deps > 0) {
  // ...
  // mount the fallback tree
  patch(
    null,
    vnode.ssFallback!,
    container,
    anchor,
    parentComponent,
    null, // fallback tree will not have suspense context
    namespace,
    slotScopeIds,
  )
  setActiveBranch(suspense, vnode.ssFallback!)
}

更新节点

更新 Suspense 组件的逻辑主要在 patchSuspense ,但是在执行更新 Suspense 逻辑前,会进行一些必要的判断,用于避免嵌套 Suspense 场景下重复渲染问题。

  • parentSuspense :存在父级 Suspense 组件

  • parentSuspense.deps > 0:父级 Suspense 存在未完成的异步依赖

  • !n1.suspense!.isInFallback 当前 Suspense 不在 fallback 状态

因为父级 Suspense 的 pendingBranch 包含了当前这个已经 resolve 的 Suspense。如果现在直接 patch 它,会导致它里面的子组件被 mount 一次;而当父级 resolve 时,又会再次 mount,造成重复挂载的问题。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (
  parentSuspense &&
  parentSuspense.deps > 0 &&
  !n1.suspense!.isInFallback
) {
  n2.suspense = n1.suspense! // 复用旧的 suspense 实例
  n2.suspense.vnode = n2 // 更新 vnode 引用为新的 vnode
  n2.el = n1.el // 复用已有的真实 DOM 元素(el)。
  return // 直接返回,跳过 patch 流程,防止重复渲染
}

下面正式分析 patchSuspense 函数的代码实现

patchSuspense

从旧节点 n1 复用 suspense 实例到新节点 n2,避免重新创建 Suspense 边界(Suspense 上下文对象),保持状态连续性:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const suspense = (n2.suspense = n1.suspense)!

SuspenseBoundary 关联的 vnode 更新为最新节点,确保后续操作基于最新组件状态:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

suspense.vnode = n2

直接复用旧节点的 DOM 元素引用,避免不必要的 DOM 创建操作,优化性能:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

n2.el = n1.el

获取新 Suspense 节点默认插槽和 fallback 插槽的内容

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

const newBranch = n2.ssContent!
const newFallback = n2.ssFallback!

完成上述准备工作后,patchSuspense 函数大体上分为两种情况的处理,一种是存在 pendingBranch 的情况,另一种是 pendingBranch 不存在的情况:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (pendingBranch) {
  // ...
} else {
  // ...
}
存在 pendingBranch 的情况

当存在待处理分支 pendingBranch 时,将新的异步内容分支设置为当前待处理分支:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

suspense.pendingBranch = newBranch

当新内容分支与 pendingBranch 类型相同时,在离屏 dom 中对新旧分支进行更新:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (isSameVNodeType(newBranch, pendingBranch)) {
  // same root type but content may have changed.
  patch(
    pendingBranch,
    newBranch,
    suspense.hiddenContainer, // 在隐藏容器中更新
    null,
    parentComponent,
    suspense,
    namespace,
    slotScopeIds,
    optimized,
  )
}

若所有异步依赖完成(deps <= 0),立即触发 resolve()

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (suspense.deps <= 0) {
  suspense.resolve()
}

当 Suspense 处于显示 fallback 内容的状态,且非 hydration(SSR 激活)过程时,对 fallback 内容执行 patch 更新:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

else if (isInFallback) {
  // It's possible that the app is in hydrating state when patching the
  // suspense instance. If someone updates the dependency during component
  // setup in children of suspense boundary, that would be problemtic
  // because we aren't actually showing a fallback content when
  // patchSuspense is called. In such case, patch of fallback content
  // should be no op
  if (!isHydrating) {
    patch(
      activeBranch,
      newFallback,
      container,
      anchor,
      parentComponent,
      null, // fallback tree will not have suspense context
      namespace,
      slotScopeIds,
      optimized,
    )
    // 更新活跃分支为新的 fallback
    setActiveBranch(suspense, newFallback)  
}

Suspense 在异步加载过程中根节点类型被切换时,递增全局 suspenseId 作为新分支的唯一标识,使之前注册的异步回调失效:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// toggled before pending tree is resolved
// increment pending ID. this is used to invalidate async callbacks
suspense.pendingId = suspenseId++

如果 isHydrating 为 true ,则说明是在 SSR 激活完成前,用户强制切换内容,这时服务端渲染的 DOM 不再有效,需中断 SSR 激活的过程,所以需要将 isHydrating 设置为 false,将原分支(pendingBranch)标记为 activeBranch ,确保后续能正确卸载该分支,避免残留无效 DOM 节点:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (isHydrating) {
  // if toggled before hydration is finished, the current DOM tree is
  // no longer valid. set it as the active branch so it will be unmounted
  // when resolved
  suspense.isHydrating = false
  suspense.activeBranch = pendingBranch
} else {
  unmount(pendingBranch, parentComponent, suspense)
}

重置 Suspense 组件状态:

  • deps = 0:重置异步依赖计数器,新分支需重新收集依赖

  • effects.length = 0:清除旧分支的副作用队列,避免残留影响

  • 新建隐藏容器:隔离新旧分支的 DOM 操作,防止冲突

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

// reset suspense state
suspense.deps = 0
// discard effects from pending branch
suspense.effects.length = 0
// discard previous container
suspense.hiddenContainer = createElement('div')

如果 Suspense 还在 fallback 状态,则挂载新分支到隐藏容器:

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (isInFallback) {
  // already in fallback state
  // 挂载新分支到隐藏容器
  patch(
    null,
    newBranch,
    suspense.hiddenContainer,
    null,
    parentComponent,
    suspense,
    namespace,
    slotScopeIds,
    optimized,
  )
  if (suspense.deps <= 0) {
    // 无异步依赖立即完成
    suspense.resolve()
  } else {
    // 更新 fallback 插槽内容
    patch(
      activeBranch,
      newFallback,
      container,
      anchor,
      parentComponent,
      null, // fallback tree will not have suspense context
      namespace,
      slotScopeIds,
      optimized,
    )
    setActiveBranch(suspense, newFallback)
  }
}

场景一,切换回当前活跃分支,即新分支 newBranch 与当前活跃分支 activeBranch 类型相同(isSameVNodeType) ,例如从 ComponentA 切换到另一个 ComponentA 实例。直接更新现有分支,强制立即 resolve 。强制立即 resolve 会跳过 DOM 卸载/移动流程。

场景二,切换到第三方分支,新分支既不同于待处理分支(pendingBranch),也不同于活跃分支,例如从 ComponentA 切换到全新的 ComponentC 。将新分支挂载到离屏 dom (hiddenContainer)中,检查异步依赖,无依赖(deps <= 0),调用 resolve() 显示内容。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

else if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
  // toggled "back" to current active branch
  // 切换回当前活跃分支
  patch(
    activeBranch,
    newBranch,
    container,
    anchor,
    parentComponent,
    suspense,
    namespace,
    slotScopeIds,
    optimized,
  )
  // force resolve
  // 强制 resolve
  suspense.resolve(true)
} else {
  // switched to a 3rd branch
  // 切换到第三方分支
  patch(
    null,
    newBranch,
    suspense.hiddenContainer, // 离屏 dom
    null,
    parentComponent,
    suspense,
    namespace,
    slotScopeIds,
    optimized,
  )
  if (suspense.deps <= 0) {
    suspense.resolve()
  }
}
不存在 pendingBranch 的情况

如果 activeBranch 存在且 newBranchactiveBranch 是同类型节点(isSameVNodeType),说明只是内容更新,不涉及根节点切换,直接 patch 并设置新的 activeBranch

否则,说明根节点发生了切换(如异步内容和 fallback 互换),则触发 onPending 事件,通知外部 Suspense 进入 pending 状态。

将新的内容分支 newBranch 作为 pendingBranch,并分配唯一的 pendingId

如果新分支是被 KeepAlive 包裹的组件(即 shapeFlag 包含 COMPONENT_KEPT_ALIVE),则直接复用该组件实例上的 suspenseId,保证同一个被缓存的组件在多次切换时 pendingId 一致,有利于缓存和恢复。

否则,为当前 Suspense 分支分配一个全局自增的 suspenseId,确保每次切换分支时 pendingId 唯一。

然后,将新分支内容挂载到离屏 dom 中,避免异步的内容在加载完成前影响真实的 dom,之后检查异步依赖计数(suspense.deps),如果为 0,说明新分支没有异步依赖,直接 resolve,将加载完成的异步内容移动到真实 dom 容器中。

如果大于 0,说明有异步依赖,进入等待流程,如果设置了 timeout ,则在超时后自动调用 suspense.fallback(newFallback),切换到 fallback 分支。如果 timeout 为 0,立即切换到 fallback 分支。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

if (activeBranch && isSameVNodeType(newBranch, activeBranch)) {
  // root did not change, just normal patch
  // 根节点未改变,仅执行标准虚拟 DOM 更新流程
  patch(
    activeBranch,
    newBranch,
    container,
    anchor,
    parentComponent,
    suspense,
    namespace,
    slotScopeIds,
    optimized,
  )
  setActiveBranch(suspense, newBranch)
} else {
  // root node toggled
  // 根节点被切换了
  // invoke @pending event
  triggerEvent(n2, 'onPending')
  // mount pending branch in off-dom container
  suspense.pendingBranch = newBranch
  // 分配唯一的 pendingId
  if (newBranch.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
    suspense.pendingId = newBranch.component!.suspenseId!
  } else {
    suspense.pendingId = suspenseId++
  }
  // 将新分支内容挂载到离屏 dom 中
  patch(
    null,
    newBranch,
    suspense.hiddenContainer,
    null,
    parentComponent,
    suspense,
    namespace,
    slotScopeIds,
    optimized,
  )
  if (suspense.deps <= 0) {
    // incoming branch has no async deps, resolve now.
    suspense.resolve()
  } else {
    const { timeout, pendingId } = suspense
    if (timeout > 0) {
      setTimeout(() => {
        if (suspense.pendingId === pendingId) {
          suspense.fallback(newFallback)
        }
      }, timeout)
    } else if (timeout === 0) {
      suspense.fallback(newFallback)
    }
  }
}

hydrate 方法分析

hydrate 方法由 hydrateSuspense 方法实现。

hydrateSuspense 方法的作用是:在 SSR(服务端渲染)场景下,将服务端渲染好的 Suspense 组件内容与客户端的虚拟节点(VNode)进行"水合"对齐,并调用 createSuspenseBoundary 方法创建 Suspense 上下文对象。

然后,调用 hydrateNode 方法对 Suspense 默认插槽,即内容分支(vnode.ssContent!)进行水合,将服务端渲染生成的 DOM 节点与虚拟节点(VNode)关联起来。

判断异步依赖计数(suspense.deps),如果异步依赖计数为 0,则调用 resolve 方法,Suspense 组件进入 resolved 状态。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

function hydrateSuspense(
  node: Node,
  vnode: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  slotScopeIds: string[] | null,
  optimized: boolean,
  rendererInternals: RendererInternals,
  hydrateNode: (
    node: Node,
    vnode: VNode,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    slotScopeIds: string[] | null,
    optimized: boolean,
  ) => Node | null,
): Node | null {
  // 创建 Suspense 上下文对象
  const suspense = (vnode.suspense = createSuspenseBoundary(
    vnode,
    parentSuspense,
    parentComponent,
    node.parentNode!,
    // eslint-disable-next-line no-restricted-globals
    document.createElement('div'),
    null,
    namespace,
    slotScopeIds,
    optimized,
    rendererInternals,
    true /* hydrating */,
  ))
  // there are two possible scenarios for server-rendered suspense:
  // - success: ssr content should be fully resolved
  // - failure: ssr content should be the fallback branch.
  // however, on the client we don't really know if it has failed or not
  // attempt to hydrate the DOM assuming it has succeeded, but we still
  // need to construct a suspense boundary first
  // 尝试水合 Suspense 默认插槽,即内容分支,将现有 DOM 节点与虚拟节点关联起来。
  const result = hydrateNode(
    node,
    (suspense.pendingBranch = vnode.ssContent!),
    parentComponent,
    suspense,
    slotScopeIds,
    optimized,
  )
  if (suspense.deps === 0) {
    // 异步依赖计数为 0 ,直接进入 resolved 状态
    suspense.resolve(false, true)
  }
  return result
}

normalize 方法分析

normalize 方法由 normalizeSuspenseChildren 方法实现。

normalizeSuspenseChildren 方法的作用是规范化 Suspense 组件的子节点,将其拆分为内容分支(ssContent)和fallback 分支(ssFallback),以便后续渲染和切换。

首先判断子节点类型,如果 vnode.shapeFlag 包含 SLOTS_CHILDREN ,说明 children 是插槽对象,分别取 defaultfallback 作为内容和 fallback 分支。

否则,直接将 children 作为内容分支,fallback 分支用注释节点(createVNode(Comment))占位。

调用 normalizeSuspenseSlot 进一步规范化内容和 fallback 分支,确保它们都是单根节点的 VNode。

结果赋值到 vnode.ssContentvnode.ssFallback,供 Suspense 运行时逻辑使用。

ts 复制代码
// packages/runtime-core/src/components/Suspense.ts

function normalizeSuspenseChildren(vnode: VNode): void {
  const { shapeFlag, children } = vnode
  const isSlotChildren = shapeFlag & ShapeFlags.SLOTS_CHILDREN
  vnode.ssContent = normalizeSuspenseSlot(
    isSlotChildren ? (children as Slots).default : children,
  )
  vnode.ssFallback = isSlotChildren
    ? normalizeSuspenseSlot((children as Slots).fallback)
    : createVNode(Comment)
}

总结

Suspense 是用于处理异步组件的场景,处理异步,自然会用到 Promise 。其实 Suspense 的本质就是使用 Promise 来处理异步组件的场景。

整体的实现流程是,Suspense 发现有异步依赖时,由于异步依赖本质是 Promise ,则会在 then 回调中获取异步依赖最终解析的结果,在异步依赖解析期间,则会把 fallback 的内容展示到页面上,当在 then 回调中拿到异步依赖最终解析的结果后,则会用异步依赖解析后的内容替换掉 fallback 的内容。

异步依赖指的是带有异步 setup() 钩子的组件和异步组件

Suspense 内部有个变量叫 deps ,用于记录异步依赖的个数,每解析完成一个异步依赖,则 deps 数减 1 ,当 deps 为 0 时,表示异步依赖已经全部解析完成,此时就可以把 fallback 的内容替换掉。Suspense 组件使用这种方式实现对多个异步依赖的等待。

参考

卧槽,牛逼!vue3的组件竟然还能"暂停"渲染!

Vue3 新特性 Teleport Suspense实现原理

相关推荐
我有一棵树8 小时前
浏览器/用户代理默认样式、any-link 伪类选择器
前端·css·html
江城开朗的豌豆8 小时前
玩转小程序页面跳转:我的路由实战笔记
前端·javascript·微信小程序
前端 贾公子8 小时前
Vue 响应式高阶 API - effectScope
前端·javascript·vue.js
幸运黒锦鲤8 小时前
npm 扩展Vite、Element-plus 、Windcss、Vue Router
前端·npm·node.js
IT_陈寒9 小时前
Java性能优化:3个90%开发者都忽略的高效技巧,让你的应用提速50%!
前端·人工智能·后端
^O^ ^O^9 小时前
pc端pdf预览
前端·javascript·pdf
艾小码9 小时前
还在纠结用v-if还是v-show?看完这篇彻底搞懂Vue渲染机制!
javascript·vue.js
徐同保9 小时前
js class定义类,私有属性,类继承,子类访问父类的方法,重写父类的方法
前端·javascript·vue.js
SUPER526614 小时前
FastApi项目启动失败 got an unexpected keyword argument ‘loop_factory‘
java·服务器·前端