likes
comments
collection
share

React源码系列(六):render阶段beginWork流程解析

作者站长头像
站长
· 阅读数 65

前言

这是React源码系列专栏的第六篇文章,预计写10篇左右,之前的文章请查看文末,通过本专栏的学习,相信大家可以快速掌握React源码的相关概念以及核心思想,向成为大佬的道路上更近一步; 本章我们学习render阶段beginWork流程,本系列源码基于v18.2.0版本;

render阶段的主要工作是构建Fiber树和生成effectList。

初始化阶段

我们首先来看一下React初始化代码。

import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<App />);

从上面的代码可以看出,它主要用于ReactDomRoot实例化,生成根对象root,不作为重点不细展开。

const root = ReactDOM.createRoot(document.getElementById('root'))

React源码的起点在root.render方法,它会调用updateContainer方法。

// react/packages/react-reconciler/src/ReactFiberReconciler.old.js
ReactDOMHydrationRoot.prototype.render = 
  ReactDOMRoot.prototype.render = function( children: ReactNodeList){
    updateContainer(children, root, null, null);
  }

接下来我们去看看 updateContainer 里面的逻辑,这里面大部分逻辑简单看一下或者忽略,你只需要关注注释地方所做的事情即可。

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  const current = container.current;
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(current);

  if (enableSchedulingProfiler) {
    markRenderScheduled(lane);
  }

  const context = getContextForSubtree(parentComponent);
  if (container.context === null) {
    container.context = context;
  } else {
    container.pendingContext = context;
  }

  const update = createUpdate(eventTime, lane);
  update.payload = {element};

  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }
  // 将 update 入队
  const root = enqueueUpdate(current, update, lane);
  if (root !== null) {
    // 调度 fiberRoot 
    scheduleUpdateOnFiber(root, current, lane, eventTime);
    entangleTransitions(root, current, lane);
  }
  // 返回当前节点(fiberRoot)的优先级
  return lane;
}

updateContainer 简单看一下,你要明白以下三点:

  • 请求当前 Fiber 节点的 lane(优先级);
  • 结合 lane(优先级),创建当前 Fiber 节点的 update 对象,并将其入队;
  • 调度当前节点(rootFiber);

我们继续进入scheduleUpdateOnFiber方法,scheduleUpdateOnFiber即为render阶段的入口,下面调用方法简单表示一下,你只需要看到调用workLoopConcurrent即可,在后续调度章节会讲到。

export function scheduleUpdateOnFiber(
  root: FiberRoot,
  fiber: Fiber,
  lane: Lane,
  eventTime: number
) {
  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    
    /** ...省略... */

    // 调用ensureRootIsScheduled方法
    ensureRootIsScheduled(root, eventTime);
    
   /** ...省略... */
  }
}


function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  
 /** ...省略... */

 // 调用performSyncWorkOnRoot方法
 scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));

 /** ...省略... */
}

function performConcurrentWorkOnRoot(root) {
  /** ...省略... */

  // 调用renderRootConcurrent方法
  let exitStatus = shouldTimeSlice
    ? renderRootConcurrent(root, lanes)
    : renderRootSync(root, lanes);
}


function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
  
	/** ...省略... */
  
  do {
    try {
      // 调用workLoopConcurrent方法
      workLoopConcurrent();
      break;
    } catch (thrownValue) {
      handleError(root, thrownValue);
    }
  } while (true);

  /** ...省略... */
  
}

workLoopConcurrent 做的事情就是通过 while 循环反复判断 workInProgress 是否为空,判断条件是否存在shouldYield的执行,如果浏览器没有足够的时间,那么会终止while循环,并在不为空的情况下针对它执行 performUnitOfWork 函数。

// react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  • workInProgress:新创建的workInProgress fiber
  • performUnitOfWork:workInProgress fiber会和已经创建的Fiber连接起来形成Fiber树。这个过程为深度优先遍历,分为 两个阶段

通过循环调用 performUnitOfWork 来触发 beginWork,新的 Fiber 节点就会被不断地创建。

// /react/packages/react-reconciler/src/ReactFiberWorkLoop.old.js
function performUnitOfWork(unitOfWork: Fiber): void {
  
   /** ...省略... **/
  
   next = beginWork(current, unitOfWork, subtreeRenderLanes);

   /** ...省略... **/
}

render阶段流程

render阶段分为两个流程。

  • 递阶段:从根节点rootFiber开始,遍历到叶子节点,每次遍历到的节点都会执行beginWork,并且传入当前Fiber节点,然后创建或复用它的子Fiber节点,并赋值给workInProgress.child。
  • 归阶段:在归阶段遍历到子节点之后,会执行completeWork方法,执行完成之后会判断此节点的兄弟节点存不存在,如果存在就会为兄弟节点执行completeWork,当全部兄弟节点执行完之后,会向上冒泡到父节点执行completeWork,直到rootFiber。

进入beginWork流程

我们对照着下面这张beginWork的图来梳理一下流程。 React源码系列(六):render阶段beginWork流程解析 beginWork主要的工作是创建或复用子fiber节点,beginWork的逻辑非常长,我们提取一下主要的逻辑。

// react/packages/react-reconciler/src/ReactFiberBeginWork.old.js
function beginWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes // 先忽略,后面会讲
): Fiber | null {
  //  current 节点不为空的情况下,会加一道辨识,看看是否有更新逻辑要处理
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
    // 若 props 更新或者上下文改变,则认为需要"接受更新"
    if (
      oldProps !== newProps ||
      hasLegacyContextChanged() ||
      (__DEV__ ? workInProgress.type !== current.type : false)
    ) {
      // 打更新标记
      didReceiveUpdate = true;
    } else {
      
      // 处理其他种情况,可忽略
      
      const hasScheduledUpdateOrContext = checkScheduledUpdateOrContext(
        current,
        renderLanes
      );
      if (
        !hasScheduledUpdateOrContext &&
        (workInProgress.flags & DidCapture) === NoFlags
      ) {
        didReceiveUpdate = false;
        return attemptEarlyBailoutIfNoScheduledUpdate(
          current,
          workInProgress,
          renderLanes
        );
      }
      if ((current.flags & ForceUpdateForLegacySuspense) !== NoFlags) {
        didReceiveUpdate = true;
      } else {
        didReceiveUpdate = false;
      }
    }
  } else {
    didReceiveUpdate = false;
    if (getIsHydrating() && isForkedChild(workInProgress)) {
      const slotIndex = workInProgress.index;
      const numberOfForks = getForksAtLevel(workInProgress);
      pushTreeId(workInProgress, numberOfForks, slotIndex);
    }
  }
  workInProgress.lanes = NoLanes;

  // switch 是 beginWork 中的核心逻辑,
  // 根据tag来创建不同的fiber 最后进入reconcileChildren函数
  switch (workInProgress.tag) {
    // 这里省略掉大量case逻辑处理

    // ...省略...
    
    // 根节点将进入这个逻辑
    case HostRoot:
      return updateHostRoot(current, workInProgress, renderLanes);
    // dom 标签对应的节点将进入这个逻辑
    case HostComponent:
      return updateHostComponent(current, workInProgress, renderLanes);
    // 文本节点将进入这个逻辑
    case HostText:
      return updateHostText(current, workInProgress);

    // ...省略...
  }

  // 处理 switch 匹配不上的情况
  throw new Error(
    `Unknown unit of work tag (${workInProgress.tag}). This error is likely caused by a bug in ` +
      'React. Please file an issue.'
  );
}

上面代码提到了节点类型HostRoot,HostComponent等组件类型,在文件ReactWorkTags.js文件中有定义,如下代码:

// react/packages/react-reconciler/src/ReactWorkTags.js
export const FunctionComponent = 0;
export const ClassComponent = 1;
export const IndeterminateComponent = 2; // Before we know whether it is function or class
export const HostRoot = 3; // Root of a host tree. Could be nested inside another node.
export const HostPortal = 4; // A subtree. Could be an entry point to a different renderer.
export const HostComponent = 5;
export const HostText = 6;
export const Fragment = 7;
export const Mode = 8;
export const ContextConsumer = 9;
export const ContextProvider = 10;
export const ForwardRef = 11;
export const Profiler = 12;
export const SuspenseComponent = 13;
export const MemoComponent = 14;
export const SimpleMemoComponent = 15;
export const LazyComponent = 16;
export const IncompleteClassComponent = 17;
export const DehydratedFragment = 18;
export const SuspenseListComponent = 19;
export const ScopeComponent = 21;
export const OffscreenComponent = 22;
export const LegacyHiddenComponent = 23;
export const CacheComponent = 24;
export const TracingMarkerComponent = 25;

首次渲染时除了rootFiber外,current 等于 null,因为首次渲染dom还没构建出来,在update时current不等于 null,因为update时dom树已经存在了,beginWork函数中用current !== null来判断是update还是mount来分别进入不同的处理逻辑。

  • mount:根据fiber.tag进入不同fiber的创建函数,最后都会调用到reconcileChildren创建子Fiber
  • update:在构建workInProgress的时候,当满足条件时,会复用current Fiber来进行优化,也就是进入bailoutOnAlreadyFinishedWork的逻辑,能复用didReceiveUpdate变量是false

beginWork的重点总结:

  • beginWork 的入参是一对用 alternate 连接起来的 workInProgress 和 current 节点,renderLanes先忽略;
  • beginWork 的核心逻辑是根据 fiber 节点(workInProgress)的 tag 属性的不同,调用不同的节点创建函数。

reconcileChildren

switch (workInProgress.tag) 中处理处理函数都会通过调用 reconcileChildren 方法,生成当前节点的子节点,然后继续深度优先遍历它的子节点执行相同的操作。

// react/packages/react-reconciler/src/ReactFiberBeginWork.old.js

export function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  // 判断 current 是否为 null
  if (current === null) {
    // 若 current 为 null,则进入 mountChildFibers 的逻辑
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
      renderLanes,
    );
  } else {
    // 若 current 不为 null,则进入 reconcileChildFibers 的逻辑
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
      renderLanes,
    );
  }
}

从源码来看,reconcileChildren 也只是做逻辑的分发,具体的工作还要到 mountChildFibers 和 reconcileChildFibers 里去看。

mountChildFibers/reconcileChildFibers

reconcileChildren会根据 current === null 条件来进入mountChildFibers 或 reconcileChildren函数中。它们会调用ChildReconciler函数。

export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);

reconcileChildFibers和mountChildFibers最终其实就是ChildReconciler传递不同的参数返回的函数,这个参数用来表示是否追踪副作用,在ChildReconciler中用shouldTrackSideEffects来判断是否为对应的节点打上effectTag,因此 reconcileChildFibers 和 mountChildFibers 的不同,在于对副作用的处理不同;

// react/packages/react-reconciler/src/ReactChildFiber.old.js
function ChildReconciler(shouldTrackSideEffects) {

  // 插入节点的逻辑
  function placeChild(
    newFiber: Fiber,
    lastPlacedIndex: number,
    newIndex: number
  ): number {
    newFiber.index = newIndex;
    if (!shouldTrackSideEffects) {
      newFiber.flags |= Forked;
      return lastPlacedIndex;
    }
    const current = newFiber.alternate;
    if (current !== null) {
      const oldIndex = current.index;
      if (oldIndex < lastPlacedIndex) {
        newFiber.flags |= Placement;
        return lastPlacedIndex;
      } else {
        return oldIndex;
      }
    } else {
      newFiber.flags |= Placement;
      return lastPlacedIndex;
    }
  }

  /** ...省略一大段逻辑...**/

  // 将总的 reconcileChildFibers 函数返回
  return reconcileChildFibers;
}

ChildReconciler 中定义了大量如 placeXXX、deleteXXX、updateXXX、reconcileXXX 等这样的函数,这些函数覆盖了对 Fiber 节点的创建、增加、删除、修改等动作,将直接或间接地被 reconcileChildFibers 所调用;

在有副作用的情况下会给Fiber 节点打上一个flags的标记,像下面这样。

newFiber.flags |= Placement;

在Fiber上打effectTag标记是为了在commit阶段执行对应dom的增删改,而且在reconcileChildren的时候,rootFiber是存在alternate的,即rootFiber存在对应的current Fiber,所以rootFiber会走reconcileChildFibers的逻辑,所以shouldTrackSideEffects等于true会追踪副作用,最后为rootFiber打上Placement的effectTag,然后将dom一次性插入,提高性能。

effectTag在源码中使用二进制表示,通过二进制表示effectTag,可以方便的使用位操作为fiber.effectTag赋值多个effect。

// react/packages/react-reconciler/src/ReactFiberFlags.js
export const NoFlags = /*                      */ 0b00000000000000000000000000;
export const PerformedWork = /*                */ 0b00000000000000000000000001;
export const Placement = /*                    */ 0b00000000000000000000000010;
export const Update = /*                       */ 0b00000000000000000000000100;
export const Deletion = /*                     */ 0b00000000000000000000001000;
export const ChildDeletion = /*                */ 0b00000000000000000000010000;
export const ContentReset = /*                 */ 0b00000000000000000000100000;
export const Callback = /*                     */ 0b00000000000000000001000000;
export const DidCapture = /*                   */ 0b00000000000000000010000000;
export const ForceClientRender = /*            */ 0b00000000000000000100000000;
export const Ref = /*                          */ 0b00000000000000001000000000;
export const Snapshot = /*                     */ 0b00000000000000010000000000;
export const Passive = /*                      */ 0b00000000000000100000000000;
export const Hydrating = /*                    */ 0b00000000000001000000000000;
export const Visibility = /*                   */ 0b00000000000010000000000000;
export const StoreConsistency = /*             */ 0b00000000000100000000000000;

在源码的ReactFiberFlags.js文件中,用二进制位运算来判断是否存在Placement,例如让let a = NoFlags,如果需要在a上增加Placement的effectTag,就只要 effectTag | Placement就可以了。如果在添加一个Update副作用直接在后面 | Update 即可。这种处理不仅速度快,而且简洁方便。

const NoFlags = 0b00000000000000000000000000;
const Placement =0b00000000000000000000000010; 
const Update = 0b00000000000000000000000100;
const PlacementAndUpdate = 0b00000000000000000000000110;
let a = NoFlags
a = a | Placement
a === Placement
a = a | Update
a === PlacementAndUpdate

React源码系列(六):render阶段beginWork流程解析

bailoutOnAlreadyFinishedWork

如果进入了bailoutOnAlreadyFinishedWork复用的逻辑,优先级足够则进入cloneChildFibers否则返回null,这里涉及优先级,会在后面章节讲到,下面代码不用重点关注。

function bailoutOnAlreadyFinishedWork(
  current: Fiber | null,
  workInProgress: Fiber,
  renderLanes: Lanes,
): Fiber | null {
  if (current !== null) {
    workInProgress.dependencies = current.dependencies;
  }

  if (enableProfilerTimer) {
    stopProfilerTimerIfRunning(workInProgress);
  }

  markSkippedUpdateLanes(workInProgress.lanes);

  if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
    if (enableLazyContextPropagation && current !== null) {
      lazilyPropagateParentContextChanges(current, workInProgress, renderLanes);
      if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
        return null;
      }
    } else {
      return null;
    }
  }
  cloneChildFibers(current, workInProgress);
  return workInProgress.child;
}

源码查看技巧

这里说一个实用的看源码技巧,就是安装一个插件BookMarks,将主要流程的函数都打上标记,方便回过头快速查找。 React源码系列(六):render阶段beginWork流程解析 React源码系列(六):render阶段beginWork流程解析

小结

本章我们学习了render阶段beginWork流程,接下来的文章将进入render阶段completeWork阶段源码分析,欢迎继续跟随本专栏一起学习;

参考链接

React源码系列