zhangyu1818.

Tag »React

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

2021年 2月3日在Github上查看

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        beginWork
p          beginWork
span       beginWork
span       completeWork
p          completeWork
p          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个子阶段

  • before mutation阶段 操作dom
  • mutation阶段 操作dom
  • layout阶段 操作dom后

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