Bootstrap

React 的源码与原理解读(四):updateContainer 内如何通过深度优先搜索构造 Fiber 树

写在专栏开头(叠甲)

  1. 作者并不是前端技术专家,也只是一名喜欢学习新东西的前端技术小白,想要学习源码只是为了应付急转直下的前端行情和找工作的需要,这篇专栏是作者学习的过程中自己的思考和体会,也有很多参考其他教程的部分,如果存在错误或者问题,欢迎向作者指出,作者保证内容 100% 正确,请不要将本专栏作为参考答案。

  2. 本专栏的阅读需要你具有一定的 React 基础、 JavaScript 基础和前端工程化的基础,作者并不会讲解很多基础的知识点,例如:babel 是什么,jsx 的语法是什么,需要时请自行查阅相关资料。

  3. 本专栏很多部分参考了大量其他教程,若有雷同,那是作者抄袭他们的,所以本教程完全开源,你可以当成作者对各类教程进行了整合、总结并加入了自己的理解。

本一节的内容

本节的我们将从 上一节留下的问题出发,谈谈 render() 中的 updateContainer 做了什么工作,他怎么样把 传入进去的 DOM 元素变成我们的 Fiber 元素。因为这部分涉及到大量进程调度的问题,我们暂时不谈论这些问题,只是简单谈谈从 updateContainer 开始到最后变成 Fiber 经历的一系列的函数链,以及他们分别做了什么工作,进程调度的全部内容我们会在之后详细谈谈

updateContainer 的调用

上一节中,我们对 render 的流程讲解停在这里,我们讲到我们把挂载的节点和 FiberRootNode 放到了 updateContainer 中,这个函数负责生成 Fiber 的逻辑

ReactDOMRoot.prototype.render = function (children) {
    const root = this._internalRoot;
    if(root === null) {
        throw new Error('Cannot update an unmounted root.');
    }
    // 渲染 
    updateContainer(children, root, null, null);
}

那么我们来看看 updateContainer 做了什么,他的源码在这个位置:

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberReconciler.js

我们大概来看看,这里省略部分 dev 模式代码

export function updateContainer(
  element: ReactNodeList,
  container: OpaqueRoot,
  parentComponent: ?React$Component<any, any>,
  callback: ?Function,
): Lane {
  // 获取 FiberRootNode 的根节点
  const current = container.current;
    
  //创建一个更新
  const eventTime = requestEventTime();
  const lane = requestUpdateLane(current);
  const update = createUpdate(eventTime, lane);
  update.payload = { element };

  // 处理 callback,不过从React18开始,render不再传入callback了,即这里的if就不会再执行了
  callback = callback === undefined ? null : callback;
  if (callback !== null) {
    update.callback = callback;
  }

  //把创建的添加到 current 的更新链表中
  enqueueUpdate(current, update, lane);

  //根据优先级和创建一个更新任务,取出 element 构造下一个 Fiber
  const root = scheduleUpdateOnFiber(current, lane, eventTime);
  if (root !== null) {
    entangleTransitions(root, current, lane);
  }
  return lane;
}

这个函数里:

  • 首先取出了我们的 FiberRootNode 的根节点,结合 lane(优先级)信息创建了一个更新,关于这个优先级我们稍后会说
  • 之后将 update 添加到根节点的更新链表中,您可以回去阅读第二章 Fiber 的结构,您可以发现在 Fiber 中有一个 updateQueue 字段( 更新列表 ),createUpdate 字段就是把更新写入这个字段,在这个更新里,我们放入了我们传入的 React Element
  • 最后我们调用了 scheduleUpdateOnFiber ,它刚刚放入的 React Element 取出,构建出下一个 fiber 节点

scheduleUpdateOnFiber 的调用

下面我们来看看 scheduleUpdateOnFiber 做了什么,它在源码的这个位置:

https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js

省略一些优先级和调度相关的代码,最后我们可以简化整个流程为:

  • 自底向上更新整个优先级并会返回更新后的整个 Fiber 树的根节点
  • 标记 Root 的更新
  • 之后 ensureRootIsScheduled 这个函数传入根节点注册一个调度任务
export function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
): FiberRoot | null {
  // 检查是否有循环更新
  checkForNestedUpdates();
  // 自底向上更新整个优先级
  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    return null;
  }

  // 标记 root 有更新,将 update 的 lane 插入到 root.pendingLanes 中
  markRootUpdated(root, lane, eventTime);

  // 省略进程调度相关的,暂时按下不表
  if (
    (executionContext & RenderContext) !== NoLanes &&
    root === workInProgressRoot
  ) {
    //.....省略
  } else {
    //.....省略进程调度相关的
      
    // 注册调度任务, 由 Scheduler 调度, 进行 Fiber 构造
    ensureRootIsScheduled(root, eventTime);
    if (
      lane === SyncLane &&
      executionContext === NoContext &&
      (fiber.mode & ConcurrentMode) === NoMode &&
      !(__DEV__ && ReactCurrentActQueue.isBatchingLegacy)
    ) {
      //.....同步相关的暂时按下不表
    }
  }
  return root;
}

ensureRootIsScheduled 生成两类调度任务

那么接下来我们进入 ensureRootIsScheduled 这个函数,它和 scheduleUpdateOnFiber 在一个目录下,他主要做了两部分的事情,第一是判断我们是不是需要注册新的调度,然后注册一个新的调度来执行我们的 Fiber 生成逻辑,我们可以看到,根据各种情况的分支最后都调用了performSyncWorkOnRoot 或者 performConcurrentWorkOnRoot 函数。

function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  const existingCallbackNode = root.callbackNode;
  //  判断是否需要注册新的调度 
  markStarvedLanesAsExpired(root, currentTime);
  const nextLanes = getNextLanes(
    root,
    root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  );

  if (nextLanes === NoLanes) {
    if (existingCallbackNode !== null) {
      cancelCallback(existingCallbackNode);
    }
    root.callbackNode = null;
    root.callbackPriority = NoLane;
    return;
  }

  // 启动一个新的调度任务
  const newCallbackPriority = getHighestPriorityLane(nextLanes);
  const existingCallbackPriority = root.callbackPriority;
  // .....省略 DEV 模式代码

  if (existingCallbackNode != null) {
    cancelCallback(existingCallbackNode);
  }

  let newCallbackNode;
  //判断是不是同步任务
  if (newCallbackPriority === SyncLane) {
    // 判断是不是 legacy 模式 ( 老版本 )
    if (root.tag === LegacyRoot) {
	  // ...省略进程调度
      //同步任务调用 scheduleLegacySyncCallback 和 performSyncWorkOnRoot
      scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
    } else {
      //同步任务调用 scheduleSyncCallback 和 performSyncWorkOnRoot
      scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
    }
    // ...省略
    newCallbackNode = null;
  } else {
    // ...省略进程调度
    // 可中断的任务调用 scheduleCallback 和 performConcurrentWorkOnRoot
    newCallbackNode = scheduleCallback(
      schedulerPriorityLevel,
      performConcurrentWorkOnRoot.bind(null, root),
    );
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

处理 SyncWork 和 ConcurrentWork

从这两个名字可以看出,我们的任务现在被分为了两类,一个是 SyncWork ,另一个是 ConcurrentWork ,他们分别对应同步任务和可中断任务:

  • 同步任务就是我们需要等待这个任务完成的任务,类似 React 15.X 以及之前的模式,我们再它结束之前不能中断它
  • 而可中断任务就是 React 16.X 之后的模式,我们可以在任务每个片执行完毕后中断它去执行优先级更高的任务,之后再继续执行它

这两个函数也有一条自己对应的调用链,这些函数的代码也都在同一个文件中,需要的可以自行阅读相关的内容:

performConcurrentWorkOnRoot —— renderRootConcurrent —— workLoopConcurrent

performSyncWorkOnRoot —— renderRootSync —— workLoopSync

在两个renderRoot 函数中,我们可以观察到,他们都需要调用 prepareFreshStack 这个函数,它的作用是:

我们之前提到过,React 中有 currentWorkInProgress 两棵树,他们通过 alternate 属性相互指引,但是在首次调用 render 方法的时候,我们的只初始化了 current 树,我们的 WorkInProgress 树还是空,但是在后续的操作中,我们需要同时用到这两棵树,所以我们需要通过当前的 current 树(只有根节点)来初始化一个 WorkInProgress 树,以下是这个函数的代码:

function prepareFreshStack(root: FiberRoot, lanes: Lanes): Fiber {
  root.finishedWork = null;
  root.finishedLanes = NoLanes;
  workInProgressRoot = root;
    
  //创建一个新的 rootWorkInProgress 树,逻辑和 current 树的创建方法类似  
  const rootWorkInProgress = createWorkInProgress(root.current, null);
  workInProgress = rootWorkInProgress;
  workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  workInProgressRootExitStatus = RootInProgress;
  workInProgressRootFatalError = null;
  workInProgressRootSkippedLanes = NoLanes;
  workInProgressRootInterleavedUpdatedLanes = NoLanes;
  workInProgressRootRenderPhaseUpdatedLanes = NoLanes;
  workInProgressRootPingedLanes = NoLanes;
  workInProgressRootConcurrentErrors = null;
  workInProgressRootRecoverableErrors = null;

  return rootWorkInProgress;
}

在这个函数中,createWorkInProgress 利用传入的 current 节点创建出 WorkInProgress 的根节点;它判断 current.alternate 是否为空,若为空则创建出一个新节点;若不为空,则复制一份节点的属性,然后将传入的属性给到这个节点。之后把 workInProgress 指针指向到初始化出来的根节点,这个指针我们后面还会用到。

之后我们来到 workLoopConcurrentworkLoopSync 这两个函数中,可以看到他们的逻辑几乎一致,首先需要判断 workInProgress 是不是存在,也就是我们刚刚初始化出来的 workInProgress 树,唯一的区别是 workLoopConcurrent 还需要判断 shouldYield 的值,简单的来说 shouldYield 就是判定有没有中断我们当前任务的因素存在,这个我们后面会提到,现在你只需要知道这两种任务的执行是有区别的就行了:

function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWork(workInProgress);
  }
}
function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}

performUnitOfWork

根据上面的代码,我们可以看到 performUnitOfWork 是两个函数都调用的公共的函数,也就是说两种任务最后都是调用了 performUnitOfWork 这个函数来处理,而它传入了我们刚刚初始化的 workInProgress 树, 现在我们来看看这个函数:

function performUnitOfWork(unitOfWork: Fiber): void {
  //取出 current 树,传入的是 workInProgress 树的 root 节点,所以它的 alternate 指向的是 current 树的 root 节点
  const current = unitOfWork.alternate;
  setCurrentDebugFiberInDEV(unitOfWork);

 //调用 beginWork 创建 Fiber 节点,它是遍历整个 element 后得到的下一个节点,第一次调用则是我们传入的 element 的根节点
  let next;
  if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
    startProfilerTimer(unitOfWork);
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
    stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
  } else {
    next = beginWork(current, unitOfWork, subtreeRenderLanes);
  }
  resetCurrentDebugFiberInDEV();
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  //如果没有下一个节点了(遍历结束了),那么结束整个流程,否则将 workInProgress 更新为刚刚新建的那个节点
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }

  ReactCurrentOwner.current = null;
}

在这个函数中, beginWork 会根据当前传入节点中的 element 结构,创建出新的 Fiber 节点,然后更新我们的 current 树和 workInProgress 树,如果它返回 null ,说明它没有孩子了,执行 completeUnitOfWork 处理函数,否则更新我们的 workInProgress,根据上文的逻辑,如果我们的 workInProgress 不是 null,那么我们将继续调用 performUnitOfWork 函数。至于 beginWork 函数内部做了什么,我们将放到下一篇中讲解。

completeUnitOfWork

当 element 中所有的子元素被遍历完毕,我们就将调用 completeUnitOfWork 函数进行处理,我们来看看这个函数,这里我们判断这个元素有没有兄弟节点,如果有兄弟节点,那么把 workInProgress 设为兄弟节点,按照上文的逻辑,此时 workInProgress 不是 null 了,整个 beginWork 的流程会继续执行。

如果没有兄弟节点了,那么把 completedWork 设置为当前节点的父节点,因为遍历的结束条件是 completedWork 不是空,也就是 completedWork 不是根节点,那么如果父节点有兄弟的话,函数就继续执行。直到返回的当前节点的父节点是根节点,说明遍历完成。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork;
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return; // 该节点的父级节点
	//省略....
    //如果当前节点还有兄弟节点,获得兄弟
    const siblingFiber = completedWork.sibling;
    if (siblingFiber !== null) {
      //继续执行我们的 beginWork
      workInProgress = siblingFiber;
      return;
    }
    //返回父节点
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  //遍历结束,标记遍历完成
  if (workInProgressRootExitStatus === RootInProgress) {
    workInProgressRootExitStatus = RootCompleted;
  }
}

一个简单的例子

也就是说 performUnitOfWorkcompleteUnitOfWork 合作完成了一个深度优先搜索的逻辑,我们来看一个例子,比如下面的组件:

const FuncComponent = () => {
  return (
    <p>
      <span>this is function component</span>
    </p>
  );
};

class ClassComponent extends React.Component {
  render() {
    return <p>this is class component</p>;
  }
}

function App() {
  return (
    <div className="App">
      <FuncComponent />
      <ClassComponent />
      <div>
        <span>123</span>
      </div>
    </div>
  );
}

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

在这里例子中,我们的遍历顺序是:

  1. 首先是进入 performUnitOfWork, App 组件,它自顶向下遍历第一个孩子,依次是 APP , div , FuncComponent , p , span , this is function component
  2. 此时我们的元素的孩子是 null 了,进入 completeUnitOfWork 的函数,此时节点没有兄弟,所以返回上一级的节点,它也没有兄弟,继续向上,直到返回到 FuncComponent ,它有兄弟节点,那么把 workInProgress 设置成兄弟 ClassComponent ,继续遍历
  3. ClassComponent 有孩子节点,依次遍历 p,this is class component,然后进入 completeUnitOfWork ,当前节点依旧没有兄弟,依次返回到 ClassComponent,然后把当前节点置为其兄弟 div
  4. 依次遍历 div ,span , 123,再次进入 completeUnitOfWork ,这次遍历依次返回父亲节点路上均没有兄弟,直到返回到根节点,遍历结束

注:这张图来自网上大神

请添加图片描述

总结

这一节里我们主要就讲解了 updateContainer 的函数调用链,它怎么样将我们传入的 React Element 变成 Fiber 树:

  • 首先它调用了 createUpdate 创建了一个更新,将我们的 element 放到了更新中
  • 之后调用 scheduleUpdateOnFiber ,根据自顶向上更新整个优先级,然后注册一个调度任务
  • 然后调用 ensureRootIsScheduled ,根据任务的类型分别生成对应的任务,同步任务或者可中断任务
  • 两类任务分别调用各自的三个函数,完成了生成 WorkInProgress 树的准备工作,然后进入 workLoop 循环
  • 在两个任务的 workLoop 循环中,都调用了 performUnitOfWork 函数来操作 element
  • performUnitOfWorkcompleteUnitOfWork 合作完成了一个深度优先搜索的逻辑,遍历了整个 DOM 树,生成了 Fiber 树

现在我们已经理清楚了从 render 入口开始到生成整个 Fiber 的整体逻辑,它是以一个 DFS 的逻辑来遍历整个 DOM 然后生成了我们需要的 Fiber 链表树,对于整个转换的内容,我们还差两部分内容没有解决

  • beginWork 函数做了什么操作
  • 优先级和调度相关的内容我们之前都暂时忽略了,之后我们需要将这部分内容也加入我们的理解中

这两个问题将在之后的文章中一一解答。

参考资料

Wenzi 大神的教程 https://www.xiabingbao.com/post/react/jsx-to-fiber-riduz4.html

;