ZHANGYU.dev

October 14, 2023

浅析React中Mount时Fiber树的创建流程

React10.0 min to read

ReactDOM.render执行后mount的流程简单梳理一下,重点分析一下mount时Fiber节点的操作,主要是循环调用beginWorkcompleteWork,以下的内容皆建立在mount时的基础上,React版本为17.0.1。

在执行render后,会进入一个比较深的调用栈来创建FiberRootNode

image

接着会触发updateContainer函数,其中调用scheduleUpdateOnFiber发起更新调度,又会进入一个比较深的调用栈来构建Fiber树。

image

Fiber树

最多同时会有2棵Fiber树,一棵为current,是当前页面呈现的节点所对应的Fiber树,一棵为workInProgressRoot,是正在更新的Fiber

两个Fiber节点之间通过alternate属性来连接,通过当前是否存在current节点来判断当前是mount阶段还是update阶段。

render执行时,首先会通过createFiberRoot函数创建FiberRootNode,同时会通过createHostRootFiber创建rootFiber并且挂在FiberRootNodecurrent属性上,后续mount时首先处理的就是rootFiber

Fiber树的构建顺序

Fiber树的构建是深度优先,先向下一直构建子节点(child),当没有子节点的时候尝试构建当前节点的兄弟节点(sibling),兄弟节点也没有时候返回父级节点(return)

image

renderRootSync

在最上面的调用栈图片中可以看到renderRootSync函数为workLoopSync的上层函数,这里会调用prepareFreshStack函数初始化workInProgressRootworkInProgress,并赋值一些全局变量

  workInProgressRoot = root;  workInProgress = createWorkInProgress(root.current, null); // 返回一个fiber节点  // 省略...

workLoopSync

ReactDOM.render调用的workLoopSync函数是同步的,会一直调用performUnitOfWork来构建一棵完整的fiber树,这个阶段被称为render阶段

function workLoopSync() {  // Already timed out, so perform work without checking if we need to yield.  while (workInProgress !== null) {    performUnitOfWork(workInProgress);  }}

performUnitOfWork

performUnitOfWork函数处理每一个Fiber节点,其中有两大阶段,一个是beginWork,一个是completeWorkbeginWork会返回当前workInProgress节点的child作为下一个待处理的节点,completeWork会将workInProgress指向兄弟(sibling)或父级(return)节点

function performUnitOfWork(unitOfWork: Fiber): void {  const current = unitOfWork.alternate;  let next;    // 省略...  // beginWork会返回下一个待处理的fiber节点  next = beginWork(current, unitOfWork, subtreeRenderLanes);    unitOfWork.memoizedProps = unitOfWork.pendingProps;  if (next === null) {    completeUnitOfWork(unitOfWork); // completeWork的上层函数  } else {    workInProgress = next; // 还有child  }  // 省略...}

child节点执行是由beginWork处理,向siblingreturn节点执行是由completeWork处理,依照上图的例子,执行过程如下:

div        beginWorkp          beginWorkspan       beginWorkspan       completeWorkp          completeWorkp          beginWork     (第2个p标签)p          completeWork  (第2个p标签)div        completeWork

beginWork

beginWork 函数在mount时会根据对应的tag来创建fiber节点

function beginWork(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes, ): Fiber | null {  // 通过current判断是否是update  if (current !== null)  // 省略update时...  // mount时  switch (workInProgress.tag) {    // 未确定类型组件    // 在mount时实际函数组件会在这个case    case IndeterminateComponent: {      return mountIndeterminateComponent(        current,        workInProgress,        workInProgress.type,        renderLanes,      );    }    // 省略其他类型...    case HostRoot:      // fiberRootNode都current节点进入updateHostRoot      return updateHostRoot(current, workInProgress, renderLanes);    case HostComponent:      // 原生标签      return updateHostComponent(current, workInProgress, renderLanes);       // 省略其他类型...  }}

performUnitOfWork第一次执行的Fiber节点为rootFiber,该fiber节点的tagHostRoot,会进入updateHostRoot函数

updateHostRoot

updateHostRoot函数只处理rootFiber,主要执行了processUpdateQueue来执行一个更新队列,执行reconcileChildren来为workInProgress产生一个child fiber节点

为什么这里会执行一个更新队列呢?我目前也没搞明白,猜测可能与React DevTools还有ssr相关。 这个更新队列会产生一个类型为ReactElement的变量,传递给reconcileChildren调用来产生子fiber节点

function updateHostRoot(current, workInProgress, renderLanes) { // 省略一些代码...  const nextProps = workInProgress.pendingProps;  cloneUpdateQueue(current, workInProgress);  processUpdateQueue(workInProgress, nextProps, null, renderLanes);  const nextState = workInProgress.memoizedState;  const nextChildren = nextState.element; // ReactElement对象,也就是child   // 省略很多代码...  reconcileChildren(current, workInProgress, nextChildren, renderLanes); // 产生子fiber节点  return workInProgress.child;}

reconcileChildren

reconcileChildren函数在mount时会创建子fiber节点

function reconcileChildren(  current: Fiber | null,  workInProgress: Fiber,  nextChildren: any,  renderLanes: Lanes,) {  // mount  if (current === null) {    workInProgress.child = mountChildFibers(      workInProgress,      null,      nextChildren,      renderLanes,    );  } else {     // 省略update...  }}

mountChildFibers是由高阶函数ChildReconciler产生的,其中主要实现reconcileChildFibers方法,该方法会判断传入的children是单个元素、多个元素、数组、还是字符串数字等等类型

function reconcileChildFibers(    returnFiber: Fiber,    currentFirstChild: Fiber | null,    newChild: any,    lanes: Lanes,  ): Fiber | null {    const isObject = typeof newChild === 'object' && newChild !== null;    // 多个children的时候也是object只是不匹配$$typeof会匹配后面的isArray    if (isObject) {      switch (newChild.$$typeof) {        // 单个元素        case REACT_ELEMENT_TYPE:          return placeSingleChild(            reconcileSingleElement(              returnFiber,              currentFirstChild,              newChild,              lanes,            ),          );         // 省略很多代码...      }    }        // 字符串数字    if (typeof newChild === 'string' || typeof newChild === 'number') {      return placeSingleChild(        reconcileSingleTextNode(          returnFiber,          currentFirstChild,          '' + newChild,          lanes,        ),      );    }    // 数组    if (isArray(newChild)) {      return reconcileChildrenArray(        returnFiber,        currentFirstChild,        newChild,        lanes,      );    }    // 省略很多代码...  }

在mount时,currentFirstChild参数固定为null,如果是上面图片的结构,这里的newChild就是为div的ReactElement,则会进入单一元素的判断,返回reconcileSingleElement的执行结果

reconcileSingleElement

reconcileSingleElement中会调用createFiberFromElement来根据element类型创建fiber节点

  function reconcileSingleElement(    returnFiber: Fiber,    currentFirstChild: Fiber | null,    element: ReactElement,    lanes: Lanes,  ): Fiber {    const key = element.key;    let child = currentFirstChild;    while (child !== null) {       // 复用的判断,mount时child固定为null不会走这里    }        // 省略Fragment...    const created = createFiberFromElement(element, returnFiber.mode, lanes);    created.ref = coerceRef(returnFiber, currentFirstChild, element);    created.return = returnFiber;    return created;  }

createFiberFromElement方法最终会执行createFiberFromTypeAndProps方法

function createFiberFromElement(  element: ReactElement,  mode: TypeOfMode,  lanes: Lanes,): Fiber {  let owner = null;  const type = element.type;  const key = element.key;  const pendingProps = element.props;  const fiber = createFiberFromTypeAndProps(    type,    key,    pendingProps,    owner,    mode,    lanes,  );  return fiber;}
createFiberFromTypeAndProps

createFiberFromTypeAndProps方法里会判断非常多的类型,这里只保留函数组件、类组件、原生标签的判断

 function createFiberFromTypeAndProps(  type: any, // React$ElementType  key: null | string,  pendingProps: any,  owner: null | Fiber,  mode: TypeOfMode,  lanes: Lanes,): Fiber {  let fiberTag = IndeterminateComponent;  // The resolved type is set if we know what the final type will be. I.e. it's not lazy.  let resolvedType = type;  if (typeof type === 'function') {    // 通过prototype.isReactComponent判断是不是类组件    if (shouldConstruct(type)) {      fiberTag = ClassComponent;    }  } else if (typeof type === 'string') {    fiberTag = HostComponent;  }  // 省略很多代码..  // 如Fragment、Suspense的判断  const fiber = createFiber(fiberTag, pendingProps, key, mode);  fiber.elementType = type;  fiber.type = resolvedType;  fiber.lanes = lanes;  return fiber;}

这里有一个需要注意的地方,就是fiberTag默认为IndeterminateComponenttype为类组件或者是原生标签的时候才会改变tag,也就是说函数组件最后创建的fiber节点的tag为IndeterminateComponent,这会使函数组件的fiber在mount时走的是mountIndeterminateComponent方法

如上面的图结构的jsx这里创建的fiber节点是div,会把此fiber节点赋值给workInProgress.child,在下一次beginWork执行此fiber节点时,会进入updateHostComponent方法

updateHostComponent

在React里会走到updateHostComponent的fiber节点肯定是多的莫法

function updateHostComponent(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,) {  // 省略...  const type = workInProgress.type;  const nextProps = workInProgress.pendingProps;  const prevProps = current !== null ? current.memoizedProps : null;  let nextChildren = nextProps.children;  const isDirectTextChild = shouldSetTextContent(type, nextProps); // 判断children是不是字符串、数字、InnerHTML等  if (isDirectTextChild) {    nextChildren = null; // 这是react对文字节点对优化,可以少创建一个text类型对fiber节点  }   // 省略...  markRef(current, workInProgress); // 标记是否有ref  reconcileChildren(current, workInProgress, nextChildren, renderLanes);  return workInProgress.child;}

updateHostComponent方法里同样会调用之前提到的reconcileChildren方法来生成子fiber节点,在updateHostComponent对文本节点有一个优化对操作,少创建一个节点。 具体来讲就是type为textareaoptionnoscriptchildrenstringnumber,或者有dangerouslySetInnerHTML的fiber节点。

completeWork

当深度遍历子节点完毕以后,会执行completeWork创建dom元素,同时在上层函数completeUnitOfWork中将workInProgress指向兄弟(sibling)或父级(return)节点

function completeWork(  current: Fiber | null,  workInProgress: Fiber,  renderLanes: Lanes,): Fiber | null { // 省略...       // 创建dom实例    const instance = createInstance(      type,      newProps,      rootContainerInstance,      currentHostContext,      workInProgress,    );        // append child    appendAllChildren(instance, workInProgress, false, false);        workInProgress.stateNode = instance;        // 设置props的属性如style,文本节点的children这个时候也会赋值    if (      finalizeInitialChildren(        instance,        type,        newProps,        rootContainerInstance,        currentHostContext,      )    ) {     // 当有autoFocus当时候才需要打上update当flag      markUpdate(workInProgress);    }  // 省略...}

completeWork最后返回到rootFiber了以后,会在上层函数performSyncWorkOnRoot中执行commitRoot方法,到这里render阶段结束。

commitRoot

commitRoot标志着render阶段结束,进入commit阶段,在commit阶段里,会触发生命周期和useLayoutEffectuseEffect钩子,同时会生成fiber节点所对应的dom节点

function commitRoot(root) {  const renderPriorityLevel = getCurrentPriorityLevel();  runWithPriority(    ImmediateSchedulerPriority,    commitRootImpl.bind(null, root, renderPriorityLevel),  );  return null;}

commitRoot实际是调用了scheduler调度器包里的方法来执行commitRootImpl

commitRootImpl

commitRootImpl方法里就是commit阶段的代码了,其中又分为3个子阶段

到这里的时候fiber树已经构建完成了