IWSR / react-code-debug

react v18 源码分析
7 stars 1 forks source link

React Suspense 源码解析 #12

Open IWSR opened 1 year ago

IWSR commented 1 year ago

React Suspense 源码解析

说明

  1. React 版本 v18.1.0

  2. 我们称 React.lazy 引入的组件为 lazyComponent , Suspense 上的 fallback 内的组件为 fallbackComponent

  3. 分析基于下述代码

    /**
    * src/components/suspense/OtherComponent.js
    */
    import React from 'react';
    
    function OtherComponent() {
     return (
       <div>
         OtherComponent
       </div>
     );
    }
    
    export default OtherComponent;
    
    /**
    * src/components/suspense/index.js
    */
    import React, { Suspense } from 'react';
    
    const OtherComponent = React.lazy(() => import('./OtherComponent'));
    
    function SuspenseTest() {
     return (
       <div>
         <Suspense fallback={<div>Loading...</div>}>
           <OtherComponent />
         </Suspense>
       </div>
     );
    }
    
    export default SuspenseTest;

TLNR

在 beginWork 创建 fiber 的过程中第一次遭遇 lazyComponent 时会 throw 一个 pending 状态的 promise,该 promise 对象会被外置的 try...catch... ( sync 模式下为 renderRootSync 内、concurrent 模式下为 renderRootConcurrent 内) 捕获,随后向上遍历将途径的所有 fiber 节点打上Incomplete 标签直到 Suspense 节点为止(completeUnitOfWork),再从该节点重新向下进行 beginWork 创建 fiber 而此时将创建 Suspense 的 fallback 节点,随后在 mutation 阶段 (commitMutationEffects) 对先前抛出的 promise 增加成功回掉以支持 异步组件成功加载后重新渲染。

React.lazy

React.lazy 在对 Suspense 的分析过程中并不是什么重要的点,因此只需要参考其 ts 声明了解出入参类型。

function lazy<T extends ComponentType<any>>(
    factory: () => Promise<{ default: T }>
): LazyExoticComponent<T>;

如果对示例代码中的 OtherComponent 打点,你可以更直观的看到它的值,其中我们只需要关注 $$typeof 即可。

OtherComponent

The first pass

标题取自 updateSuspenseComponent 内的注释,该函数是在 beginWork 运行至 Suspense 节点时被调用,其内部逻辑在 First pass 中与 Second pass 中分别有着不同的逻辑(在 The second pass 中其会创建 LazyComponent 上的 fallback 对应的节点),不过在 The first pass 中其会先创建 offScreen,文中的例子在经过 First pass 阶段后,我们可以获得下图结构的 fiber 树

fiber

其中 lazy 这一 fiber 对应的就是通过 React.lazy 引入的 OtherComponent,而此时它还只是一个占位符,不能被称为一个组件。当 beginWork 运行至 lazy 处时才会调用引入组件的逻辑(代码在 mountLazyComponent)。

function mountLazyComponent(
  _current,
  workInProgress,
  elementType,
  renderLanes,
) {
  resetSuspendedCurrentOnMountInLegacyMode(_current, workInProgress);

  const props = workInProgress.pendingProps;
  const lazyComponent: LazyComponentType<any, any> = elementType;
  const payload = lazyComponent._payload;
  const init = lazyComponent._init;
  let Component = init(payload);
  ...
  // 底下的不用关注,init 函数内就会通过 throw 中断,见下面的代码
}

// mountLazyComponent 内调用的 init
function lazyInitializer<T>(payload: Payload<T>): T {
  if (payload._status === Uninitialized) {
    // 此时的 payload._result 为 import OtherComponent 部分,其调用将返回一个 pending 的 promise
    const ctor = payload._result;
    const thenable = ctor();
    // 此处为 promise 实例增加了回掉,但加载成功之后重新渲染的逻辑并不在此处
    thenable.then(
      moduleObject => {
        if (payload._status === Pending || payload._status === Uninitialized) {
          // Transition to the next state.
          const resolved: ResolvedPayload<T> = (payload: any);
          resolved._status = Resolved;
          resolved._result = moduleObject;
        }
      },
      error => {
        if (payload._status === Pending || payload._status === Uninitialized) {
          // Transition to the next state.
          const rejected: RejectedPayload = (payload: any);
          rejected._status = Rejected;
          rejected._result = error;
        }
      },
    );
    if (payload._status === Uninitialized) {
      // first pass 时会进入此逻辑,将状态由 Uninitialized 修改为 Pending
      const pending: PendingPayload = (payload: any);
      pending._status = Pending;
      pending._result = thenable;
    }
  }
  if (payload._status === Resolved) {
    const moduleObject = payload._result;
    ...dev 内容
    return moduleObject.default;
  } else {
    // first pass 将会将 pending 状态的 promise 丢出去
    // 并被外置 try catch 捕获
    throw payload._result;
  }
}

上面代码块 ctor 对应的函数

ctor

对应的调用栈

错误捕获 —— throwException

错误捕获发生在 renderRoot 的 handleError 内,在该函数内的 throwException 会先将 lazy fiber 打上 Incomplete 的标记并找到最近的 Suspense 节点打上 shouldCapture 的标记(该标记将在 The second pass 内起作用)。 将会从 lazy fiber 处开始向上遍历并将沿途的所有 fiber 节点打上 Incomplete 标记,直到遇到 Suspense 节点结束(Suspense 节点除了Incomplete 外还会被标记 shouldCapture,)

如果将整段 handleError 贴上来分析会很长,因此下面我只截取关键步骤以证明

给 lazyComponent 标记 Incomplete

查找最近的 SuspenseComponent 标记 shouldCapture

throwException 内的一些小细节

ConcurrentMode 下的 attachPingListener

attachPingListener 只有在 ConcurrentMode 下才会被调用,因为异步组件的请求有可能在 react 进入 commit 阶段时便返回,而在 ConcurrentMode 模式下是允许高优先级任务插队的,如果返回请求所对应的优先级高于正在进行中的任务,那么就会被插队处理

function attachPingListener(root: FiberRoot, wakeable: Wakeable, lanes: Lanes) {
  // 底下是维护 pingCache 的逻辑,并非阅读重点
  let pingCache = root.pingCache;
  let threadIDs;
  if (pingCache === null) {
    pingCache = root.pingCache = new PossiblyWeakMap();
    threadIDs = new Set();
    pingCache.set(wakeable, threadIDs);
  } else {
    threadIDs = pingCache.get(wakeable);
    if (threadIDs === undefined) {
      threadIDs = new Set();
      pingCache.set(wakeable, threadIDs);
    }
  }
  if (!threadIDs.has(lanes)) {
    // Memoize using the thread ID to prevent redundant listeners.
    threadIDs.add(lanes);
    // ping 函数内会通过对 ensureRootIsScheduled 的调用来触发任务调度
    // ensureRootIsScheduled 的解析可以看我的 [useState](https://github.com/IWSR/react-code-debug/issues/3) 的解析
    const ping = pingSuspendedRoot.bind(null, root, wakeable, lanes);
    if (enableUpdaterTracking) {
      if (isDevToolsPresent) {
        // If we have pending work still, restore the original updaters
        restorePendingUpdaters(root, lanes);
      }
    }
    // 此处的 wakeable 是之前 pending 的 promise,也就是说 ConcurrentMode 模式下异步组件的成功请求也可能触发 ensureRootIsScheduled
    wakeable.then(ping, ping);
  }
}

attachRetryListener

attachRetryListener 主要是将 pending 状态的 promise 挂到 Suspense fiber 的 updateQueue 上,这与其他 fiber 有些不太类似(毕竟都是拿来装 update 实例构成的链表的)。

function attachRetryListener(
  suspenseBoundary: Fiber,
  root: FiberRoot,
  wakeable: Wakeable,
  lanes: Lanes,
) {
  // Retry listener
  //
  // If the fallback does commit, we need to attach a different type of
  // listener. This one schedules an update on the Suspense boundary to turn
  // the fallback state off.
  //
  // Stash the wakeable on the boundary fiber so we can access it in the
  // commit phase.
  //
  // When the wakeable resolves, we'll attempt to render the boundary
  // again ("retry").
  const wakeables: Set<Wakeable> | null = (suspenseBoundary.updateQueue: any);
  if (wakeables === null) {
    const updateQueue = (new Set(): any);
    updateQueue.add(wakeable);
    suspenseBoundary.updateQueue = updateQueue;
  } else {
    wakeables.add(wakeable);
  }
}

错误捕获 —— completeUnitOfWork

在 throwException 之后会调用 completeUnitOfWork,该函数在此处的作用是从 lazy Component 出发向上遍历并将沿途的所有 fiber 节点都标记上 Incomplete 标记,但当其遇到带有 ShouldCapture 标记的节点后便会将其上关于错误处理的标记消除(next.flags &= HostEffectMask)然后再从这个节点进入 beginWork。其也是 completeWork 的入口函数,是 react 中的一个关键函数。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate; // WIP 对应的 current节点
    const returnFiber = completedWork.return; // 父节点

    if ((completedWork.flags & Incomplete) === NoFlags) {
      // 如果当前节点没有出错(没有标记 Incomplete ),进入该逻辑
            ...
      let next;
      ... 删掉了不重要的部分
      // completeWork 的作用大致为生成DOM 更新props 绑定事件,详细的建议看别的文章
      next = completeWork(current, completedWork, subtreeRenderLanes);

      if (next !== null) {
        // Completing this fiber spawned new work. Work on that next.
        workInProgress = next;
        return;
      }
    } else {
      // 运行至此代表节点带有 Incomplete 标记
      // 当 unwindWork 遇到带有 ShouldCapture 标记的节点时会将该节点标记 DidCapture 后返回
      // unwindWork 内的 (flags & ~ShouldCapture) | DidCapture 这段标记的 DidCapture
      // 而带有 ShouldCapture 标记的节点通常也被称为错误边界,可以说 unwindWork 这个函数的作用
      const next = unwindWork(current, completedWork, subtreeRenderLanes);

      // Because this fiber did not complete, don't reset its lanes.

      if (next !== null) {
        /**
         * 在这个例子内 SuspenseComponent 带有 ShouldCapture 因此在遇到 SuspenseComponent 后将其上与错误处理相关的标记清除
         * 
         * 关于标记清除的逻辑可以看下面
         *
         * 0b0000000000001000000000000000 Incomplete      
         * 0b0000000000010000000000000000 ShouldCapture  
         * 0b0000000000011000000000000000 同时带有 Incomplete 与 ShouldCapture 的标记的节点
         * 0b0000000000000111111111111111 HostEffectMask
         * 0b0000000000000000000000000000 如果将上面两个标记进行按位与(=&)能得到的,很明显能看到 Incomplete 与 ShouldCapture 标记位消失了
        */
        next.flags &= HostEffectMask;
        // 此处为将 suspense 节点赋值给 workInProgress
        workInProgress = next;
        // return 出去后会再次进入 workLoopSync/workLoopConcurrent,随后再从 workInProgress 开始进行 beginWork
        // 也就是说,从这里开始我们将再此对 suspense 节点进行 beginWork,而这也代表着 The second pass 的开始
        return;
      }

      ... 省略不必要的逻辑

      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its subtree flags.
        // 给当前 fiber 的父节点标记 Incomplete
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      } else {
        // We've unwound all the way to the root.
        workInProgressRootExitStatus = RootDidNotComplete;
        workInProgress = null;
        return;
      }
    }

    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      // 如果存在兄弟节点,对兄弟节点进行 beginWork,见 workLoopSync/workLoopConcurrent 内 while 的条件
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
        // workInProgress 指向当前节点的父节点 然后继续 do...while... 循环
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // 已经到 root 节点了,标记 workInProgressRootExitStatus 为完成状态
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

The second pass

在经过 completeUnitOfWork 的处理后,Suspense 节点上已经被标记了 DidCapture,而在 The second pass 中 beginWork 会再次从 Suspense 节点开始创建 fiber 节点(updateSuspenseComponent ),由于 DidCapture 的影响,updateSuspenseComponent 也会进入 first pass 跳过的逻辑去处理 fallback 的内容。

function updateSuspenseComponent(current, workInProgress, renderLanes) {
  const nextProps = workInProgress.pendingProps;

  let suspenseContext: SuspenseContext = suspenseStackCursor.current;

  let showFallback = false;
  // 判断当前 fiber 上是否标记 DidCapture
  const didSuspend = (workInProgress.flags & DidCapture) !== NoFlags;

  if (
    didSuspend ||
    shouldRemainOnFallback(
      suspenseContext,
      current,
      workInProgress,
      renderLanes,
    )
  ) {
        // 存在 DidCapture 标记,因此需要 showFallback
    showFallback = true;
    // 将 DidCapture 从节点标记上清除
    workInProgress.flags &= ~DidCapture;
  } else {
    ... The second pass 不会进入就删了
  }

    ...

  if (current === null) {
    ... 删除 hydrate 相关代码
    // This could've been a dehydrated suspense component.
    const suspenseState: null | SuspenseState = workInProgress.memoizedState;
    ...

    const nextPrimaryChildren = nextProps.children;
    const nextFallbackChildren = nextProps.fallback;

    if (showFallback) {
      // second pass 时 showFallback 为 true
      // 这里返回的 fallback 的 fiber,底下截图有介绍内部的大致逻辑
      const fallbackFragment = mountSuspenseFallbackChildren(
        workInProgress,
        nextPrimaryChildren,
        nextFallbackChildren,
        renderLanes,
      );
      const primaryChildFragment: Fiber = (workInProgress.child: any);
      primaryChildFragment.memoizedState = mountSuspenseOffscreenState(
        renderLanes,
      );
      workInProgress.memoizedState = SUSPENDED_MARKER;
      ...
            // 接下来要创建 fallback 的节点了
      return fallbackFragment;
    } else if (
      enableCPUSuspense &&
      typeof nextProps.unstable_expectedLoadTime === 'number'
    ) {
      ...
    } else {
        ...
    }
  } else {
    ... 
}

mountSuspenseFallbackChildren 内的大致逻辑

在经过上述逻辑后,其结构如下

而在 The second pass 结束后,其 fiber 树结构为下图

关于 promise resolve 之后触发的重新渲染

当异步组件加载结束后,也就是之前那个 pending 的 promise 实例状态变更后,需要添加一个对应的成功回掉以触发更新,而这个回掉函数是在 commit 阶段(commitRootImpl)内的 mutation 小阶段(commitMutationEffects)内注册进 promise 内的,具体位置存在于 attachSuspenseRetryListeners 内。

image

图中的 retry 函数为 resolveRetryWakeable (位置在 ReactFiberWorkLoop.old.js 内)

image-20230621015020524

ensureRootIsScheduled 相关的可以看我的 [React Hooks: useState 分析](https://github.com/IWSR/react-code-debug/issues/3)