React 源码分析(二)—— Fiber 的 render 阶段

背景概述

React 原理分析(一) —— React 设计思想中说到说到,为了实现 React Diff 阶段的异步可中断FiberReact 的一次更新流程拆成了两个阶段:

  1. Render 阶段: 异步可中断的 diff 新老 DOM,找到差异后并不及时立刻更新。而是对该 Fiber 节点打上一个tag(Update/Placement/Delete)。
  2. Commit 阶段: 遍历存在 tag 的 Fiber ,根据 tag 的类型执行对应的 DOM 更新。

本文重点讲解更新流程中 render阶段的全流程 ,在 React 中,render 流程中可以被拆分为 beginWorkcompleteWork两个阶段。这里分别介绍一下两个流程的工作。

流程概览

调度时机

React 原理分析(一) —— React 设计思想 所说,React 每一次 state 更新 都会生成一个 Update 加入任务队列中 最终 Schedule 会根据任务队列对应优先级行任务调度。示意流程如下:

React中每一个 Update 都会经历 RenderCommit 的流程。Render阶段主要完成fiber 节点的创建/变化并打上对应的tagRender会从 rootFiber 节点开始进行深度优先遍历, 深度优先的过程中 递阶段 执行BeginWork函数, 归阶段 执行CompleteWork函数。

export default function App() {
  return (
    <div className="App">
      <div>
        hello
        <span> world </span>
      </div>
    </div>
  );
}
复制代码

上方 App 组件生成的对应的 Fiber 节点如下 :

对应 Fiber 的 BeginWork/CompleteWork 调度顺序如下:

  1. Root 节点的 BeginWork 阶段
  2. App 节点的 BeginWork 阶段
  3. div 节点的 BeginWork 阶段
  4. div 节点的 CompleteWork 阶段
  5. span 节点的 BeginWork 阶段
  6. span 节点的 CompleteWork 阶段 (React 内部的优化,如果只有一个 Text 结点跳过 BeginWork/CompleteWork)
  7. div 节点的 CompleteWork 阶段
  8. App 节点的 CompleteWork 阶段
  9. Root 节点的 CompleteWork 阶段

BeginWork

在整个更新流程中,ReactFiberRoot(Root) 开始深度遍历。对每一个Fiber节点执行一次beginWork。最终会返回一个Child FiberbeginWork总体概括如下:

React 根据来对workInProgress节点和对应的current节点进行Diff 并打上对应的 Flag

扫描二维码关注公众号,回复: 13452201 查看本文章

BeginWork 函数主流程

BeginWork方法执行于 performUnitOfWork 函数中。具体逻辑如下:

// 根据双缓存机制, current 为当前页面渲染的 Fiber 结点,在 mount 阶段时为 null。workInProgress 为本次需要渲染在 浏览器上的 Dom 结点
// renderLanes 为本次更新的优先级, 优先级相关内容在后期会讲述到。在这里只需要知道就好了
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  // 获取本次 Update 的优先级
  const updateLanes = workInProgress.lanes;
  // 根据 current 判断是 mount 还是 update 
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    if (oldProps !== newProps || hasLegacyContextChanged()) {
      didReceiveUpdate = true;
    } else if (!includesSomeLane(renderLanes, updateLanes)) {
      // Fiber Update 优先级 与 本次调度 Update 优先级不符合
      didReceiveUpdate = false;
      ...
      return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
    }
  } else {
    didReceiveUpdate = false;
  }
  workInProgress.lanes = NoLanes;
  // 根据 Fiber 类型,走向不同的分支
  switch (workInProgress.tag) {
    // 一个函数组件,第一次 mount 时会走到 IndeterminateComponent(匿名组件逻辑中),
    // 执行完后,tag 会根据是否存在render函数判断是否为 Class 组件和 Function 组件
    case IndeterminateComponent: { // 类型未知的组件
      return mountIndeterminateComponent(
        current,
        workInProgress,
        workInProgress.type,
        renderLanes,
      );
    }
    case FunctionComponent: { // 函数 组件
      ...
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case ClassComponent: { // Class 组件
      ... 
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    case HostRoot: // FiberRoot 节点
      return updateHostRoot(current, workInProgress, renderLanes);
    case HostComponent: // DOM节点对应的 Fiber 
      return updateHostComponent(current, workInProgress, renderLanes);
    case HostText: // Text 节点
      return updateHostText(current, workInProgress);
  }
}
复制代码

根据 current 是否存在判断是 Mount 阶段还是 Update 阶段

根据React的双缓存机制可知 current Fiber 会储存当前页面渲染的 Fiber 节点。 如果该节点为空。则表示当前页面没有对应的 DOM节点 , 为 Mount 阶段, 否则为 Update 阶段。

判断节点是否需要复用

React 内部根据 didReceiveUpdate 判断节点是否需要复用。 如果该变量为 false 。 则直接复用之前 Fiber 的 DOM节点

根据 tag 走不同的挂载流程

beginWork最终会根据不同的 tag 走向不同的分支逻辑, 这些函数的主要逻辑大同小异,主要逻辑都是创建对应的 Child Fiber,并为当前节点/子节点打上 Placement/Deletion/Update 标记该Fiber需要 放置/删除/更新。

挂载 函数组件 流程

这里以 IndeterminateComponent 类型为例。当首次挂载函数组件时,函数组件的tagIndeterminateComponent。 在 mountIndeterminateComponent 中,会执行函数组件的逻辑,生成一个 Fiber 及其对应的DOM 结构。

function mountIndeterminateComponent(
    _current,
    workInProgress,
    Component,
    renderLanes,
  ) {
    // 如果是 mount 阶段,将当前 Fiber 的 alternate 清空。
    // 给当前 Fiber 打上 Placement 的标记。
    if (_current !== null) {
      _current.alternate = null;
      workInProgress.alternate = null;
      // Since this is conceptually a new fiber, schedule a Placement effect
      workInProgress.flags |= Placement;
    }
    const props = workInProgress.pendingProps;
    let value;
    // 执行 函数组件, 获取对应的 DOM 结构
    value = renderWithHooks(
        null,
        workInProgress,
        Component,
        props,
        context,
        renderLanes,
    )
    // React DevTools reads this flag.
    workInProgress.flags |= PerformedWork;
    // 将 tag 置成 FunctionComponent
    workInProgress.tag = FunctionComponent;
    // Diff 算法,为 Child Fiber 打上 Placement/Update/Delete 的Flag
    reconcileChildren(null, workInProgress, value, renderLanes);
    // 返回下一个需要 beginWork 的 Fiber 节点, 如果为 null 表示当前
    // Fiber 没有子节点可以继续 beginWork 了, 则从该节点开始 beginWork 
    return workInProgress.child;
  }  
复制代码

其余的 updateFunctionComponent/ updateClassComponent 逻辑类似,不再解释。

CompleteWork

BeginWork 结束时,会返回当前 Fiber 节点的 child 节点。如果 child 节点为 null 时,说明当前节点不能再向下遍历了,这时便会执行当前节点的 completeUnitOfWork 逻辑。 completeWork 函数的主要作用如下:

  • 针对 typeHostRoot , HostComponent , HostText 等存在真实 DOMFiber 节点。将其 DOM 依次从子节点插入父节点的DOM 中, 并赋值 stateNode 属性。
  • 将存在 Placement / Deletion / Updatetag 的串成一个链表, 方便 commit 阶段对 Fiber 进行处理。
function performUnitOfWork(unitOfWork: Fiber): void {
  // The current, flushed, state of this fiber is the alternate. Ideally
  // nothing should rely on this, but relying on it here means that we don't
  // need an additional field on the work in progress.
  const current = unitOfWork.alternate;
  let next;
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    unitOfWork.memoizedProps = unitOfWork.pendingProps;
    if (next === null) {
    // 如果是 Fiber 树的叶子结点, 则执行 completeWork 
      completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
  ReactCurrentOwner.current = null;
}
复制代码

DOM 结构的创建与插入

completeWork 会将字节点的 DOM 依次插入父节点的 DOM 中,并保存在 stateNode 属性中。并在 commit 阶段将其渲染在浏览器上。代码如下:

    case HostComponent: {
      popHostContext(workInProgress);
      const rootContainerInstance = getRootHostContainer();
      const type = workInProgress.type;
      if (current !== null && workInProgress.stateNode != null) {
        // Update 阶段, 更新 Fiber 
        updateHostComponent(
          current,
          workInProgress,
          type,
          newProps,
          rootContainerInstance,
        );
        if (current.ref !== workInProgress.ref) {
          markRef(workInProgress);
        }
      } else {
          // 创建 当前 Fiber 对应的 DOM 节点
          const instance = createInstance(
            type,
            newProps,
            rootContainerInstance,
            currentHostContext,
            workInProgress,
          );
          // 将 子节点对应的 DOM 节点加入当前 Fiber 中
          appendAllChildren(instance, workInProgress, false, false);
          workInProgress.stateNode = instance;
         if (
            finalizeInitialChildren(
              instance,
              type,
              newProps,
              rootContainerInstance,
              currentHostContext,
            )
          ) {
            markUpdate(workInProgress);
          }
        if (workInProgress.ref !== null) {
          // If there is a ref on a host node we need to schedule a callback
          markRef(workInProgress);
        }
      }
      return null;
    }
  // 将 workInPrgress 的节点挂载在 parent 对应的 DOM 节点上
  const appendAllChildren = function(
    parent: Instance,
    workInProgress: Fiber,
    needsVisibilityToggle: boolean,
    isHidden: boolean,
  ) {
    // We only have the top Fiber that was created but we need recurse down its
    // children to find all the terminal nodes.
    let node = workInProgress.child;
    while (node !== null) {
      // 如果当前 Fiber 是 HostComponent 或者 HostText 节点,插入 parent 节点中
      if (node.tag === HostComponent || node.tag === HostText) {
        appendInitialChild(parent, node.stateNode);
      } else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
        // 如果是一个 函数组件
        appendInitialChild(parent, node.stateNode.instance);
      } else if (node.tag === HostPortal) {
        // If we have a portal child, then we don't want to traverse
        // down its children. Instead, we'll get insertions from each child in
        // the portal directly.
      } else if (node.child !== null) {
        // 递归
        node.child.return = node;
        node = node.child;
        continue;
      }
      if (node === workInProgress) {
        return;
      }
      // 处理兄弟节点
      while (node.sibling === null) {
        if (node.return === null || node.return === workInProgress) {
          return;
        }
        node = node.return;
      }
      node.sibling.return = node.return;
      node = node.sibling;
    }
  };
复制代码

如下面 App 组件,其对应的 Fiber 的 Fiber 中挂载的 DOM 如下。

export default function App() {
  return (
    <div className="App">
      <div>
        hello
        <span> world </span>
      </div>
    </div>
  );
}
复制代码

EffectList 的建立

beginWork中,React 给所有需要变动的 Fiber 打上了对应的flag。这些flag会在commit 阶段中被消费。为了更好的对Fiber 中的flag 进行消费。 React 将其Child Fiberflag 挂在当前FibersubtreeFlagssubtreeFlags 表示,如果当前Fiber 的子节点有存在的flagsubtreeFlags就会存在对应的值。

function bubbleProperties(completedWork: Fiber) {
  let subtreeFlags = NoFlags;
  let child = completedWork.child;
  while (child !== null) {
    newChildLanes = mergeLanes(
      newChildLanes,
      mergeLanes(child.lanes, child.childLanes),
    );
    subtreeFlags |= child.subtreeFlags & StaticMask;
    subtreeFlags |= child.flags & StaticMask;
    child.return = completedWork;
    child = child.sibling;
  }
  completedWork.subtreeFlags |= subtreeFlags;
}
复制代码

经过这样的处理后,在commit阶段只需要通过Fiber上的 subtreeFlags 属性便可知道,当前Fiber下是否是有Child Fiber存在flags。提高了commit阶段的效率。

render 渲染 Error 组件

Error 组件处理流程

在上面提到, React 的组件会在beginWork 阶段执行。

从下方源码中可以看到,在 dev 阶段,当 originalBeginWork 执行失败时会走到catch 重置属性,并重新执行一次beginWork函数。

  beginWork = (current, unitOfWork, lanes) => {
    // 复制当前 Fiber , 如果 beginWork 出错了。 则用复制的 Fiber 进行 reset 
    const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
      dummyFiber,
      unitOfWork,
    );
    try {
      return originalBeginWork(current, unitOfWork, lanes);
    } catch (originalError) {
      // Keep this code in sync with handleError; any changes here must have
      // corresponding changes there
      resetContextDependencies();
      resetHooksAfterThrow();
      // Don't reset current debug fiber, since we're about to work on the
      // same fiber again.
      // Unwind the failed stack frame
      unwindInterruptedWork(unitOfWork, workInProgressRootRenderLanes);
      // Restore the original properties of the fiber.
      assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
      // Run beginWork again.
      invokeGuardedCallback(
        null,
        originalBeginWork,
        null,
        current,
        unitOfWork,
        lanes,
      );
      // We always throw the original error in case the second render pass is not idempotent.
      // This can happen if a memoized function or CommonJS module doesn't throw after first invocation.
      throw originalError;
    }
  };
复制代码

如果再一次执行 FiberbeginWork 阶段失败,则会被外层的try ... Catch 捕获住并被handleError函数进行处理。handleError 最终会走到

try {
  workLoopConcurrent();
  break;
} catch (thrownValue) {
  handleError(root, thrownValue);
}

 
function handleError(root, thrownValue): void {
  do {
    let erroredWork = workInProgress;
    try {
      // Reset module-level state that was set during the render phase.
      resetContextDependencies();
      resetHooksAfterThrow();
      resetCurrentDebugFiberInDEV();
      // TODO: I found and added this missing line while investigating a
      // separate issue. Write a regression test using string refs.
      ReactCurrentOwner.current = null;
      // 对 root Fiber 的特殊处理, 因为 ErrorBoundary 只能对子组件进行处理
      if (erroredWork === null || erroredWork.return === null) {
        // Expected to be working on a non-root fiber. This is a fatal error
        // because there's no ancestor that can handle it; the root is
        // supposed to capture all errors that weren't caught by an error
        // boundary.
        workInProgressRootExitStatus = RootFatalErrored;
        workInProgressRootFatalError = thrownValue;
        workInProgress = null;
        return;
      }
      // 最终会走到 throwException
      throwException(
        root,
        erroredWork.return,
        erroredWork,
        thrownValue,
        workInProgressRootRenderLanes,
      );
      completeUnitOfWork(erroredWork);
    } catch (yetAnotherThrownValue) {
       // 如果抛出了其他异常,则一直循环。直到处理完成
      thrownValue = yetAnotherThrownValue;
      if (workInProgress === erroredWork && erroredWork !== null) {
        // If this boundary has already errored, then we had trouble processing
        // the error. Bubble it to the next boundary.
        erroredWork = erroredWork.return;
        workInProgress = erroredWork;
      } else {
        erroredWork = workInProgress;
      }
      continue;
    }
    // Return to the normal work loop.
    return;
  } while (true);
} 
复制代码
  1. throwException 会为这个出错的 Fiber 打上 Imcompleteeffect flags。表明该 Fiberrender 阶段未完成。
  2. workInProgressRootExitStatus 置为 RootErrored 在所有 Fiber
  3. 同时会向上查找最近存在 getDerivedStateFromErrorcomponentDidCatchClass Component (下文称 ErrorBoundary 组件)作为错误边界。找到后打上 ShouldCaptureflag 表明该组件需要进行错误处理。
  4. 基于 ErrorBoundary 组件创建一个 update, getDerivedStateFromErrorupdatepayload , componentDidCatchcallback ,最终入队 ErrorBoundary 组件的 updateQueue 中。
  5. 跳出该节点的 beginWork 阶段, 进入completeWork 阶段。
function throwException(
  root: FiberRoot,
  returnFiber: Fiber,
  sourceFiber: Fiber,
  value: mixed,
  rootRenderLanes: Lanes,
) {
  // The source fiber did not complete.
  sourceFiber.flags |= Incomplete;
  if (
    value !== null &&
    typeof value === 'object' &&
    typeof value.then === 'function'
  ) {
    const wakeable: Wakeable = (value: any);
    resetSuspendedComponent(sourceFiber, rootRenderLanes);
    renderDidError();
    value = createCapturedValue(value, sourceFiber);
    let workInProgress = returnFiber;
    do {
      switch (workInProgress.tag) {
        case HostRoot: {
          const errorInfo = value;
          workInProgress.flags |= ShouldCapture;
          const lane = pickArbitraryLane(rootRenderLanes);
          workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
          const update = createRootErrorUpdate(workInProgress, errorInfo, lane);
          enqueueCapturedUpdate(workInProgress, update);
        return;
      }

      case ClassComponent:
        // Capture and retry
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        if (
          (workInProgress.flags & DidCapture) === NoFlags &&
          (typeof ctor.getDerivedStateFromError === 'function' ||
            (instance !== null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          workInProgress.flags |= ShouldCapture;
          const lane = pickArbitraryLane(rootRenderLanes);
          workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
          // Schedule the error boundary to re-render using updated state
          const update = createClassErrorUpdate(
            workInProgress,
            errorInfo,
            lane,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
        break;
      default:
        break;
    }
    workInProgress = workInProgress.return;
  } while (workInProgress !== null);
}
复制代码

最后就是 FibercompleteWork 阶段了,需要关注的是此时 FiberflagIncomplete 。这里会向上找到最近的 ErrorBoundary 组件并从该节点开始从新执行新的 beginWork 逻辑来产生 fallback 组件。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;
    // Check if the work completed or if something threw.
    if ((completedWork.flags & Incomplete) === NoFlags) {
    } else {
      // 如果 completedWork 为能处理错误的组件,则 next 为该组件,否则为 null 
      const next = unwindWork(completedWork, subtreeRenderLanes);
      // 因为该组件可以处理 Error ,将其设置为 workInProgress Fiber
      if (next !== null) {
        next.flags &= HostEffectMask;
        workInProgress = next;
        return;
      }
      // 将其父节点设置为 Incomplete ,直到找到可以处理 Error 的组件。
      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its subtree flags.
        returnFiber.flags |= Incomplete;
        returnFiber.subtreeFlags = NoFlags;
        returnFiber.deletions = null;
      }
    }
    // 如果存在兄弟节点,则将兄弟节点设置为 workInProgress Fiber
    // 否则将父节点设置为下一个 workInProgress 的组件
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      // If there is more work to do in this returnFiber, do that next.
      workInProgress = siblingFiber;
      return;
    }
    // Otherwise, return to the parent
    completedWork = returnFiber;
    // Update the next thing we're working on in case something throws.
    workInProgress = completedWork;
  } while (completedWork !== null);

  // We've reached the root.
  if (workInProgressRootExitStatus === RootIncomplete) {
    workInProgressRootExitStatus = RootCompleted;
  }
}
复制代码

流程梳理

以下面这个 Fiber 结构为例, ErrorBoundary 组件可以处理 Error,其中App组件会抛出一个错误。整个渲染流程如下

  1. Root Fiber 开始向下深度优先进行 beiginWork 的处理
  2. 处理到 App Fiber 时抛出一个 Error
  3. 重新在 App Fiber 进行beginWork处理,再次抛出Error
  4. App Fiber 打上 Incompleteflag , 找到最近的可以处理Error的组件ErrorBoudary,打上 shouldCaptrueflag。并为该 Fiber 添加上一个 payloadgetDerivedStateFromErrorupdate
  5. App 组件执行completeWork, 向上找到 ErrorBoundary 组件,ErrorBoundary 组件处理 updateQueue 获取新的 state 。根据 state 更新到 fallback UI
  6. 重新为fallback UI 执行 beginWork,按正常流程进行 render 阶段处理
  7. 进行 commit 阶段,浏览器渲染 fallback UI 和对应的DOM

往期文章

1.React 原理分析(一) —— React 设计思想

参考文章

  1. juejin.cn/post/691962…
  2. juejin.cn/post/691779…
  3. react.iamkasong.com/

猜你喜欢

转载自juejin.im/post/7035604734933205000