770 行代码还原 react fiber 初始链表构建过程

892 阅读22分钟

TLDR;

本文意在探索在 react@18.3.0 中, react 应用 mount 阶段 react fiber 初始链表的构建过程。为了探索这个过程,鲨叔呕心沥血地整理出了 770 行代码,以极简的代码量来还原了这个过程。不信我?有图为证:

image.png

还想亲身把玩一下,感受 react 源码的原汁原味?点击这里,好好享受吧!

anyway,你今天点赞了吗?

前言

无论是 stack reconciler 架构还是 fiber reconciler 架构。从应用的生命周期的视角来看,react 应用都会依次经历两个阶段:

  • mount 阶段
  • update 阶段

而无论是 mount 阶段还是 update 阶段,一个界面更新流程又都可以划为render 阶段commit 阶段,我相信这是人尽皆知的了。

其实,render 阶段前面应该还有一个 scheduling 阶段 。但是有些人又把 scheduling 阶段合并到render 阶段。鉴于还有歧义,本文还是沿用传播得比较广泛的说法吧。

而本文要探索的「react fiber 初始链表构建过程」就是位于 react 应用的 mount 阶段里面的 render 阶段。

我们就拿 create-react-app 内置的默认模板项目作为本文贯穿全文的示例。现在,我们的应用根组件是这样的:

function App() {

  return (
    <div className="App">
      <header className="App-header">
        <img src={logo} className="App-logo" alt="logo" />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="App-link"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}
  1. 那你知不知道,当这个组件初始挂载后,react 内部就会构建一个 react fiber 链表呢?
  2. 如果你知道上面这事,那你又知不知道这一条 react fiber 链表长什么样子呢?
  3. 也许你也知道 fiber 节点上是靠三个很出名的指针child,sibling,return 链接到一块的,但是你又知不知道 react 内部都是怎么实现这个链接的?

对于以上的第一和第二个疑问,我们也不卖关子,改造一下入口代码,我们就知道答案:

const root = window.ReactDOMRoot = ReactDOM.createRoot(document.getElementById("root"));

image.png

从上面截图,我们可以看出,react 应用的根组件一旦挂载后,一条完整的 fiber 链表就隐藏在root这个头指针里面。完整的 fiber 链表如下:

图示 1-1

image.png

注意,上图中,还有两个细节遗漏了。第一个细节是:hostRootFiber 的 return指针指向的是 null;第二个细节是:hostRootFiber 使用了 stateNode 属性循环引用了 FiberRootNode

也许你跟我一个样,多多少少看过不少 react fiber 架构的技术文章,但是大部分文章也只是蜻蜓点水式地介绍 react fiber 架构的方方面面,基本上都没提这个初始链表构建过程。而我觉得,这个数据结构是 react fiber 架构的基础,因而显得十分重要。

本人通过阅读 react@18.3.0 的源码,认识并理解了 react fiber 的初始链表构建过程。但是,其实在 react 源码内部,相对于我们的探索目标,是存在很多干扰性的碎片/分支代码。这些干扰性的碎片/分支代码包括:

  • 实现向后兼容 API 的代码
  • 提高开发环境下的开发体验的代码
  • 提供给开发工具使用的 API 代码
  • 性能测量和收集的 API 代码等等。
  • 跟当前探索目标无关的代码 - 比如,这次探索中,react scheduling 相关的代码就属于这类型的代码

正如,聂鲁达的《似水年华》中所说的那样:“当华美的叶片落尽,生命的脉络才历历可见。” 去除那些相对于我们探索目标的干扰代码,也许我们能够把 react 的脉络看得得更清楚。秉持这样的理念,在本文中,我严格遵循 react 当前的源码实现,通过一边解读,一边摘抄的方式,重新整理了 react fiber 初始链表的构建过程。

这么做,一来是希望加深自己对fiber 初始链表构建过程的理解,二来是希望能够去帮助那些想从 「only source of truth 」的角度去理解这方面知识的人。

正文

创建头指针和 hostRootFiber

在上述的 react fiber 链表中,ReactDOMRoot 对象充当一个 namespace 的角色,而 hostRootFiber 则充当整个 fiber 链表的头指针角色。下面,我们来看看,作为整个链表开始的部分,这两个对象是怎么创建的。

源码路径 image.png

在这里路径下,我们能看ReactDOM.createRoot()函数的实现。因为,我们只是关注整个链表的创建过程,所以,原函数中的很多跟scheduling 相关的代码都可以去除掉。这部分,我们又可以分为三步骤:

  1. 创建FiberRootNode:
function FiberRootNode(containerInfo) {
    this.tag = 1; // 这是一个 concurrent root
    this.containerInfo = containerInfo;
    this.current = null;
}
  • 对于普通 fiber 而言,fiber 节点的 tag 属性唯一标识了一个 fiber 节点的 work 的类型(通过追溯代码的提交历史,我们发现,一开始,work tag 的种类是比较少的,随着时间的推移和react 相关特性的增加,开发者就不断地追加 work tag 的种类。到目前为止,一种有以下的 26 种 work tag)。

代码片段 1

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;

但是在这里有点特殊。FiberRootNode 不是普通的 fiber,它 tag 的值不是从上面取值,而是取值于以下两种:

//@flow
export type RootTag = 0 | 1;

export const LegacyRoot = 0;
export const ConcurrentRoot = 1;

我们这里取值为1,代表着我们要开启「并发渲染模式」。当我们使用旧的写法:ReactDOM.render() 来挂载 react 应用的话,那么FiberRootNode 的 tag 就是为0,代表的使用的是 react@16 之前的「同步渲染模式」。不信?我们可以把入口代码改成:

import ReactDOM from "react-dom";

ReactDOM.render(<App />, document.getElementById("root"));

这种情况下,FiberRootNode 是挂在 root DOM 节点的 _reactRootContainer 属性上:

image.png

  • containerInfo 这里储存的就是 react 应用所挂载的那个 DOM 节点的引用。在本示例中,这个 DOM 节点就是 document.getElementById("root")
  • current 的属性值指向的是 hostRootFiber。该 fiber 节点归类为普通的 fiber 节点,使用的是上面「代码片段1」所罗列的 workTag。在 react 的源码中,它们给 hostRootFiber 打上 的 workTag 是 3 - hostRoot 类型。 hostRootFiber 是 fiber 节点树的真正根节点。下面我们来创建hostRootFiber
  1. 创建hostRootFiber。 因为 hostRootFiber 也是普通的 fiber 节点。所以,它跟所有的通过 react element 来创建出来的 fiber 一样,公用同一个 fiber 节点构造函数 FiberNode()
function FiberNode(tag, pendingProps, key, mode) {
  // Instance
  this.tag = tag;
  this.key = key;
  this.elementType = null;
  this.type = null;
  this.stateNode = null;

  // Fiber
  this.return = null;
  this.child = null;
  this.sibling = null;
  this.index = 0;

  this.ref = null;

  this.pendingProps = pendingProps;
  this.memoizedProps = null;
  this.updateQueue = null;
  this.memoizedState = null;
  this.dependencies = null;

  this.mode = mode;
 }

因为本文只关注 fiber 链表的构造过程,所以,很多跟这个目标不相关的 fiber 属性就省略掉了。从上面的函数签名,我们可以看出,构建一个 fiber 节点,只需要四个参数。下面一一说明一下:

  • tag - work 的类型标识。它是唯一标识一个 fiber 的 work 的类型。不同类型的 work 对应着要执行的 effect 是不一样的。比如说,如果一个 fiber 的 tag 值是 5,那就证明它是从 HostComponent 类型的 react element 创建而来的。它在 commit 阶段要执行的 effect 就是「DOM 节点的创建,插入,删除和属性更新」。如果一个 fiber 的 tag 值是 0,那就证明它是从 FunctionComponent 类型的 react element 创建而来的。它在 commit 阶段要执行的 effect 就是「调用函数组件的跟生命周期有关的 hook」,以上等等。
  • pendingProps - 待处理的 props。fiber 的创建和复用的「原始物料」都是 react element。因为 react element 代表着用户更新界面的最新意图。这里的 pendingProps 就是react element 上的 props属性。
  • key - 唯一标识一个组件实例。主要是用于提高 diff 算法在渲染列表时候的性能表现。key 是通过 react element 的 key 值所透传过来的。
  • mode - 当前 react 应用整体的工作模式。在不同的工作模式下,react 的行为有差异性的表现。当前可用的 mode 值有以下几个:
export type TypeOfMode = number;

export const NoMode = /*                         */ 0b000000;
// 就像源码注释这里所说的那样,对于 `ConcurrentMode` 这个值 来说,没有必要,我们完全可以到 `FiberRootNode` 节点上去读取它的 `tag`值。因为这两个字段值是一样的含义。
// TODO: Remove ConcurrentMode by reading from the root tag instead
export const ConcurrentMode = /*                 */ 0b000001;
export const ProfileMode = /*                    */ 0b000010;
export const DebugTracingMode = /*               */ 0b000100;
export const StrictLegacyMode = /*               */ 0b001000;
export const StrictEffectsMode = /*              */ 0b010000;
export const ConcurrentUpdatesByDefaultMode = /* */ 0b100000;

本文中,使用的是「并发渲染模式」,所以,我们写死为 1 即可。

经过上面的释义,我们可以整理出创建 hostRootFiber 节点的函数来:

function createHostRootFiber() {
  const mode = 1; // 1 是代表着 concurrent mode
  const hostRootworkTag  = 3; // 3 代表着这个 "hostRootFiber"
  return new FiberNode(hostRootworkTag, null, null, mode);
}
  1. 借助指针currentstateNodeFiberRootNodehostRootFiber 互相链接起来:
function createFiberRoot(containerDOM) {
  const root = new FiberRootNode(containerDOM);
  const uninitializedFiber = createHostRootFiber();
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;
  uninitializedFiber.memoizedState = {
    element: null,
    isDehydrated: false,
    cache: null,
  };

  //initializeUpdateQueue(uninitializedFiber);

  return root;
}
  1. 最后,用 namespace 角色的 ReactDOMRootFiberRootNode 包起来,同时把 FiberRootNode 的引用在 container DOM 节点那边保存一份,方便后面的快捷访问:
function ReactDOMRoot(internalRoot) {
  this._internalRoot = internalRoot;
}

const randomKey = Math.random().toString(16).slice(2);
const internalContainerInstanceKey = "__reactContainer$" + randomKey;
export function markContainerAsRoot(hostRoot, node) {
  node[internalContainerInstanceKey] = hostRoot;
}

function createRoot(containerDOM) {
  const root = createFiberRoot(containerDOM);
  markContainerAsRoot(root.current, containerDOM);
  return new ReactDOMRoot(root);
}

以上完整代码可以到这里查看。至此,我们创建了 fiber 链表的头部部分:

image.png

还剩下的部分是怎么构建的呢?毫无疑问是我们在我们调用ReactDOMRoot.render(<App />)的时候。render 阶段的起始函数是renderRootSync()(或者是 renderRootConcurrent())。我们稍微 debug 就会发现,从ReactDOMRoot.render(<App />)renderRootSync(),其实还是有一定长度的调用栈的。

image.png

其实,这段距离的调用栈就是 react 的调度(shceduler) 层。因为调度层的逻辑很复杂,且与本文主题无关,所以,我略过这一层。

在进入著名的 work loop 之前,我们先整理出一下 renderRootSync() 的代码:

let workInProgress = null;

function renderRootSync(root){
 prepareFreshStack(root);
 do {
    try {
      workLoopSync();
      break;
    } catch (error) {
      console.log(error)
    }
  } while (true);
}

function prepareFreshStack(root){
    root.finishedWork = null;
    workInProgress = root;
}

在上面的代码中,全局变量 workInProgress 可以说是整个 work loop 的「灵魂人物」。它在我们这个同步渲染的 work loop 充当一个「移动指针」,时刻指向的是下一个要执行 work 的 fiber 节点。

经过上面的代码的执行,我们当前的 fiber 链表是长这样的:

image.png

现在,我们应该带着这样的疑问来看代码:「以根 react element(<App />) 为输入,react 是怎么样构建链表的剩下部分的呢?」

work loop 的架构

现在我们有一个 hostRootFiber 和一个根 react element <App />。react 就是以这两者为「原料」,借助 work loop 架构去完成链表的链表的构建的。

work loop 架构原理看起来比较简单:就是一个while 循环(这原理跟 event loop 的实现原理很像)。在这个循环里面,我们对每个 fiber 节点去执行 work。而对一个 fiber 节点执行 work 又可以依次分为两个阶段:

  • beging work 阶段
  • complete work 阶段

这个 work loop 结束的条件是:没有下一个要执行 work 的 fiber。当 fiber 链表的上的所有 fiber 都执行了 beging work 和 complete work 之后,最后回到 hostRootFiber 节点上,此时全局变量workInProgress 就是 null

根据上面的理解,我们可以整理出这样的代码:

function workLoopSync(){
    while(workInProgress !== null){
        performUnitOfWork(workInProgress)
    }
}

function performUnitOfWork(unitOfWork) {
  const current = unitOfWork.alternate; // 因为是链表的初始构建,这里的 current 的值是 null

  const next  = beginWork(current, unitOfWork);
  unitOfWork.memoizedProps = unitOfWork.pendingProps;
  
  if (next === null) {
    completeUnitOfWork(unitOfWork);
  } else {
    workInProgress = next;
  }
}

beginWork() 主要是负责根据当前 workInProgress fiber 身上所携带的原始物料 - react element,创建一个新的 fiber 节点。然后,用childreturn 指针把这个新建的 fiber 节点和 workInProgress fiber 链接起来,实现它们之间的父子关系,最后把这个新建的 fiber 节点返回出去。

从上面performUnitOfWork()的实现代码来看,你也注意到了一个事实: next 局部变量不为空,那么它就会赋值给 workInProgress 全局变量。然后我们再回到 workLoopSync() 函数中,进入下一轮的 work loop。而下一轮的 work loop 开始后,我们首先还是进入 beginWork() 的调用。

以上,本质就是 beginWork() 函数的递归调用。相信了解过 react fiber 架构的人,都知道:react 在这里就是实现了「深度优先遍历」算法。但是,很多人不知道,这里的「深度优先遍历」算法到底是在对谁进行遍历。答案是:「react element 树」

注意,react element tree 并不是现成,而是在beginWork() 函数的递归调用的过程中跟 fiber 树同时构建的。不过,它的构建逻辑是比较简单的 - 对于 function component 或者 class component,我们直接调用它自己或者它的render()方法即可。对于 host component,我们直接访问它的props.children属性即可。通过上述方式,我们就能知道下一个用于创建 fiber 节点的 react element 是谁。下面在讲述 beginWork()函数的实现会更加具体地阐述这一点。

「递归,递归,有递必须有归」。 beginWork() 函数实现的是「递」,而completeUnitOfWork() 实现的就是「归」。

「递」- beginWork 实现深度优先遍历

在 react 的源码中,beginWork 主要是针对当前的 workInProgress fiber 节点的 tag 属性值,分门别类地去做相应的处理 - 调用对应的 helper 函数。在上述的「代码片段1」中,我们可以看到罗列的 tag 的枚举值有 26 种。在这里,我们就不一一讨论了。我们只针对本文所用的 create-react-app 模板项目所涉及到的几个类型的 component 进行分析和摘抄。所以,我们的 beginWork() 函数目前的大体架构是这样的:

function beginWork(workInProgress) {
  switch (workInProgress.tag) {
    case IndeterminateComponent: {
      const component = workInProgress.type;
      return mountIndeterminateComponent(
        current,
        workInProgress,
        component
      );
    }
    case FunctionComponent: {
      const Component = workInProgress.type;
      const resolvedProps = workInProgress.pendingProps;
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps
      );
    }
    case HostRoot:
      return updateHostRoot(current, workInProgress);
    case HostComponent:
      return updateHostComponent(current, workInProgress);
    case HostText:
      return updateHostText(current, workInProgress);

    default:
      throw new Error("Other case is ignored");
  }
}
  • mountIndeterminateComponent()
  • updateFunctionComponent()
  • updateHostRoot
  • updateHostComponent
  • HostText

以上就是我们要整理的 helper 函数。


function updateHostRoot(current, workInProgress) {
  const nextProps = workInProgress.pendingProps;
  // 下面这一行代码主要负责从 hostRootFiber 的 updateQueue 中计算出 `memoizedState`。
  // 这里为了降低代码的复杂性,我们直接用 mock 的方式来完成这一步
  // processUpdateQueue(workInProgress);
  const nextState = workInProgress.memoizedState;

  const nextChildren = nextState.element;
  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

function processUpdateQueue(workInProgress){
    workInProgress.memoizedState = {
        element: React.createElement(App)
    }
}

function updateHostComponent(current, workInProgress,) {
  const type = workInProgress.type;
  const nextProps = workInProgress.pendingProps;
  const prevProps = current !== null ? current.memoizedProps : null;
  let nextChildren = nextProps.children;

  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

function mountIndeterminateComponent(
  _current,
  workInProgress,
  Component,
  renderLanes
) {
  const props = workInProgress.pendingProps;

  const value = renderWithHooks(
    null,
    workInProgress,
    Component,
    props
  );

  if (
    // Run these checks in production only if the flag is off.
    // Eventually we'll delete this branch altogether.
    typeof value === "object" &&
    value !== null &&
    typeof value.render === "function" &&
    value.$$typeof === undefined
  ) {
    // Proceed under the assumption that this is a class instance
    // 省略 class component 的处理流程
  } else {
    // Proceed under the assumption that this is a function component
    workInProgress.tag = FunctionComponent;
    reconcileChildren(null, workInProgress, value, renderLanes);
    return workInProgress.child;
  }
}

function updateFunctionComponent(
  current,
  workInProgress,
  Component,
  nextProps
) {
  const nextChildren = renderWithHooks(
    current,
    workInProgress,
    Component,
    nextProps
  );

  reconcileChildren(current, workInProgress, nextChildren);
  return workInProgress.child;
}

function updateHostText(current, workInProgress) {
  return null;
}

以上就是摘抄出来的几个 helper 函数。如果仔细阅读,你会发现,所有的 helper 函数要实现的功能就是:

  1. 找到要创建新 fiber 的「原始物料」 - 单个 react element 或者多个 react element 的数组,并创建新 fiber 节点;
  2. 处理 workInProgress fiber 与新创建 fiber 之间的父子关系;
  3. 最后返回这个新的 fiber 节点作为下一个要处理的工作单元。

可以看出,所有的 helper 函数调用后,都会进入reconcileChildren() 函数。而这个函数实现的就是大名鼎鼎的「reconciliation 过程」(中文把 “reconciliation” 翻译为“协调”或者“调和”,我这里就不翻译了)。reconciliation 过程所应用的比对算法就是称之为「diff 算法」。

显然,在 react 应用的 mount 阶段,我们当前的界面还没有与之对应的 fiber 树的,所以,上面的 helper 函数中,我们给 reconcileChildren() 函数的 current 参数传入的值都是 null。因为在 mount 阶段,是没有什么可对比的,故而没有什么 可以复用,diff 算法最终都是走了一些新建 fiber 节点的逻辑。但是,为了代码的复用,react 还是把这个两个阶段(mount 阶段和 update 阶段)的 reconciliation 过程整合到一块。这么做是合理的,因为即使在应用的 update 阶段,也存在新增 fiber 节点的情况。

下一小节,我们不急着进入大名鼎鼎的 reconciliation 的实现过程。到目前为止,我们知道它所实现的功能就够了(上文中已经介绍了)

「归」- completeUnitOfWork 实现广度优先遍历

上面介绍的是「递」的过程,那什么时候「归」呢?很显然,当 workInProgress fiber 为 null 的时候,我们就「归」了。那什么时候下一个 workInProgress fibe 为 null 呢?react 在以下的两种情况就将下一个workInProgress fibe 这是为 null:

  • 当前的 react element 没有子节点了
  • 当前的 react element 有子节点,但是只有一个文本类型的子节点

针对第一种情况,我们很好理解,叶子节点肯定是「递」的尽头了。对于第二种情况,这种 react 内部为了性能优化所做的一种优化,我们暂且不需要理解。

function completeUnitOfWork(unitOfWork) {
  // Attempt to complete the current unit of work, then move to the next
  // sibling. If there are no more siblings, return to the parent fiber.
  let completedWork = unitOfWork;

  do {
    // 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 = completedWork.alternate;
    const returnFiber = completedWork.return; // Check if the work completed or if something threw.

    let next = completeWork(current, completedWork, renderLanes$1);

    // 这个一个特例,我们忽略不看
    //if (next !== null) {
      // Completing this fiber spawned new work. Work on that next.
      //workInProgress = next;
      //return;
    //}

    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.
}

function completeWork(current, completedWork) {
  console.log(`完成 ${JSON.stringify({type: completedWork.type, tag: completedWork.tag})} fiber 节点的 work`)
  return null;
}

在源码中,completeWork() 的函数会分为很多条件分支去做处理,但是所有的代码分支最终都是返回 null;为了简化,completeWork() 的函数实现中,我们负责把当前的 fiber 节点的相关信息打印出来,并返回 null 即可。

现在我们把目光聚焦到completeUnitOfWork()函数的函数体里面。从我们摘抄的代码来看,我们首先尝试用一个do {...} while (){} 循环去自底向上地去 complete work。但是会遇到一个特例:如果当前的 fiber 节点还有兄弟节点的话,那么就会跳出这个循环,因而也跳出了当前的 work loop,进入下一个 work loop。在进入下一个 work loop之前,我们还会修改 workInProgress 指针的指向,让它指向当前 fiber 节点的兄弟节点。

如此一来,只有当同一层级的所有的兄弟节点都 complete work ,它的父节点才会 complete work。自始至终, complete work 的方向都是「自底向上」的。在这个方向上,「返回到父节点」这件事永远都是发生在该父节点最后一个(自左向右方向的)子节点身上。

创建 fiber 节点 - reconciliation 过程

我们在上面的“「递」- beginWork 实现深度优先遍历” 小结中指出:

所有的 helper 函数调用后,都会进入reconcileChildren() 函数。而这个函数实现的就是大名鼎鼎的「reconciliation 过程

本小节,我们就来看看在 mount 阶段,这个 reconciliation 过程是怎么实现的。

function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null,
      nextChildren,
    );
  } else {
    workInProgress.child = reconcileChildFibers(
      workInProgress,
      current.child,
      nextChildren,
    );
  }
}

mountChildFibers()reconcileChildFibers() 函数的区别仅仅在于两者所闭包的变量 shouldTrackSideEffects 的值。mountChildFibers()所闭包的shouldTrackSideEffects变量值为 falsereconcileChildFibers()所闭包的shouldTrackSideEffects变量值为 true。是否追踪副作用的结果,跟给 fiber 节点打标签(「打标签」就是给 fiber 节点的 tag 属性赋值)的事情有关系。这对于我们理解 reconciliation 过程的影响不大,可以略过。

mountChildFibers()reconcileChildFibers()在源码内部对应到同一个函数实现里面去:

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

function ChildReconciler(shouldTrackSideEffects) {

  // This API will tag the children with the side-effect of the reconciliation
  // itself. They will be added to the side-effect list as we pass through the
  // children and the parent.
  function reconcileChildFibers(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChild: any
  ): Fiber | null {

    // Handle object types
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(
              returnFiber,
              currentFirstChild,
              newChild
            ),
          );
        // case REACT_PORTAL_TYPE:
        //   return placeSingleChild(
        //     reconcileSinglePortal(
        //       returnFiber,
        //       currentFirstChild,
        //       newChild,
        //       lanes,
        //     ),
        //   );
        // case REACT_LAZY_TYPE:
        //   const payload = newChild._payload;
        //   const init = newChild._init;
        //   // TODO: This function is supposed to be non-recursive.
        //   return reconcileChildFibers(
        //     returnFiber,
        //     currentFirstChild,
        //     init(payload),
        //     lanes,
        //   );
      }

      if (isArray(newChild)) {
        return reconcileChildrenArray(
          returnFiber,
          currentFirstChild,
          newChild
        );
      }

      // throwOnInvalidObjectType(returnFiber, newChild);
    }

    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      return placeSingleChild(
        reconcileSingleTextNode(
          returnFiber,
          currentFirstChild,
          '' + newChild
        ),
      );
    }
  }
}

因为本文示例项目中没有用到除了 REACT_ELEMENT_TYPE 这种类型之外的 react element,所以,我们暂时不关注这两种情况。上面的源码的三种情况刚好跟在本文示例项目中我们所遇到的情况所吻合。其实,newChild是文本或者数字类型算是「单节点 reconcile」的特性。也就是说,reconciliation 过程可以分为两种情况:

  • 单节点 reconciliation
  • 多节点 reconciliation

上面所谓的「单节点」或者「多节点」指的是在 react element 树上,同一层级子节点的个数是一个还是一个以上。

从函数签名我们就知道,reconciliation 流程的产物是一个或新建或复用的 fiber 节点。因为我们这里是 react 应用的 mount 阶段,所以,我们这里的 reconciliation 流程的产物是就是一个刚新建的 fiber 节点。下面,我们看看「单节点 reconciliation」的流程。

单节点 reconciliation

  function reconcileSingleElement(
    returnFiber,
    currentFirstChild,
    element,
  ): Fiber {
    // 因为链表的初始构建阶段,currentFirstChild 的值为 null,所以下面的代码就注释掉了
    //const key = element.key;
    //let child = currentFirstChild;
    //while (child !== null) {
      // 这里省略了很多代码
    //}

  const created = createFiberFromElement(element, returnFiber.mode, lanes);
  //created.ref = coerceRef(returnFiber, //currentFirstChild, element);
  created.return = returnFiber;
  return created;
  }
  
  function createFiberFromElement(
  element
) {

  const type = element.type;
  const key = element.key;
  const pendingProps = element.props;
  const fiber = createFiberFromTypeAndProps(
    type,
    key,
    pendingProps
  );
  
  return fiber;
}

从上面摘抄的代码我们可以看出,创建一个全新的 fiber 节点,我们需要的物料就是一个「react element」。再具体点讲,是需要两个要素:

  • react element type
  • react element props

react element type 的外一个作用是「根据它的值来创建 fiber 节点的 work tag」。这一步,具体发生在 createFiberFromTypeAndProps() 函数里面:

function createFiberFromTypeAndProps(type, key, pendingProps) {
  let fiberTag = 2; // 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") {
    if (shouldConstruct(type)) {
      fiberTag = 1; // ClassComponent;
    }
  } else if (typeof type === "string") {
    fiberTag = 5; // HostComponent;
  } else {
    // 针对 type 的其他情况的值来决定 `fiberTag` 的值或者走自己的创建新 fiber 的流程
  }

  const fiber = createFiber(fiberTag, pendingProps, key);
  fiber.elementType = type;
  fiber.type = resolvedType;

  return fiber;
}

function shouldConstruct(Component) {
  const prototype = Component.prototype;
  return !!(prototype && prototype.isReactComponent);
}

上面,我们省略了针对type值的类型为非function或者string的情况。react 在这个函数里面的实现有一点奇怪,那就是「typeof type === "function"条件分支下,应该还可以细分为两种情况 - function component 或者 class component,但是当前,react 只是处理 class component」。也就是说,function component 在这里应该打上一个值为 0 的 work tag,但是 react 没有这么做,而是选择在下一轮的 work loop 去修正这个值。目前,我还不知道为什么要这么做。

最后,我们来到应用 mount阶段 reconciliation 流程的最后一站 - 真正地去创建新 fiber 节点

const createFiber = function(
  tag,
  pendingProps,
  key
) {
  return new FiberNode(tag, pendingProps, key);
};

function FiberNode(
    tag,
    pendingProps,
    key
  ) {
    // Instance
    this.tag = tag;
    this.key = key;
    this.elementType = null;
    this.type = null;
    this.stateNode = null;
  
    // Fiber
    this.return = null;
    this.child = null;
    this.sibling = null;
    this.index = 0;
  
    this.ref = null;
  
    this.pendingProps = pendingProps;
    this.memoizedProps = null;
    this.updateQueue = null;
    this.memoizedState = null;
    this.dependencies = null;
  
    // this.mode = mode;
  
    // Effects
    this.flags = 0; // NoFlags
    // this.subtreeFlags = NoFlags;
    // this.deletions = null;
  
    // this.lanes = NoLanes;
    // this.childLanes = NoLanes;

    this.alternate = null;
  }

从创建新 fiber 节点的源代码来看,新创建的 fiber 节点,只有四个属性被赋值了:

  • type
  • elementType
  • tag
  • key

我们所关注的链表指针类型的属性:

  • return
  • child
  • sibling

此时还是为 null 的。那这三个指针的值是在什么时候决定的呢?答案是:「一路返回的时候」。

首先,对于单节点 fiber 而言,基于它的定义,它的 sibling指针的值肯定是为null

其次,return 指针的值是在 reconcileSingleElement() 函数中所指定的。

最后,child 指针的值是在 reconcileChildren() 函数中所指定的。

最后的最后,我们看看placeSingleChild():

const Placement = /*                    */ 0b00000000000000000000000010;
  function placeSingleChild(newFiber) {
    // This is simpler for the single child case. We only need to do a
    // placement for inserting new children.
    if (shouldTrackSideEffects && newFiber.alternate === null) {
      newFiber.flags |= Placement;
    }
    return newFiber;
  }

placeSingleChild()的作用就是根据所列的条件来给各个新的 fiber 节点来打副作用的标签(flags)。在本文的实例中,我们只有<App /> 这个 react element 所对应的 fiber 节点会被打上Placement 的副作用标签。我们可以据此推断一下,估计 react 会在 commit 阶段调用 DOM API appendChild() 来将它插入到 container DOM 的子节点里面。

也许,你会发现,我们目前所摘抄的 reconciliation 过程的代码没没有看到 diff 算法的踪影。这是故意而为之的,为的就是删繁就简。因为在 react 应用的 mount 阶段,我们的 current fiber 树上本身就是 null,workInProgress fiber 树根本就没有啥好对比的,所以可以讲 diff 算法其实是没有被运用到的。

最后,我们拿本文用到的模板项目来进一步说明一下。假如,当前我们的 fiber 树是这样的:

image.png

那么经过对 hostRootFiber 的 begin work之后(换句话说,走完单节点 reconciliation 流程之后),我们会得到这样的 fiber 树:

image.png

对于接下来的 react element - <div><App>,<header> 也同理 - 走了单节点的 reconciliation 流程,最终我们得到这样的一颗 fiber 链表:

image.png

多节点 reconciliation

多节点 reconciliation 走的就是 reconcileChildrenArray() 函数。在源码中,该函数的签名是:

function reconcileChildrenArray(
    returnFiber: Fiber,
    currentFirstChild: Fiber | null,
    newChildren: Array<*>,
    lanes: Lanes,
): Fiber | null

因为,我们现在是 react 应用的 mount 阶段,所以,currentFirstChild 的值是为 null 的。我们可以往上追溯到传值的源头 reconcileChildren()里面

function reconcileChildren(
  current: Fiber | null,
  workInProgress: Fiber,
  nextChildren: any,
) {
  if (current === null) {
    workInProgress.child = mountChildFibers(
      workInProgress,
      null, //  这个值会传给 `reconcileChildrenArray()` 的 `currentFirstChild` 形参
      nextChildren,
    );
  } else {
    //workInProgress.child = reconcileChildFibers(
    //  workInProgress,
    //  current.child,
    //  nextChildren,
    //);
  }
}

可以看到,因为是在应用的 mount 阶段,所以,current fiber 是为 null。而 currentFirstChild 的值来自于current.child。所以,这里,react 是硬编码传入了 null

因为在 reconcileChildren() 里面,currentFirstChild 的值为 null,那么我可以把几乎一半的不会执行到的代码给移除掉,最后得到:

  function reconcileChildrenArray(
    returnFiber,
    currentFirstChild,
    newChildren,
  ) {

    let resultingFirstChild = null;
    let previousNewFiber = null;

    let oldFiber = currentFirstChild;
    let lastPlacedIndex = 0;
    let newIdx = 0;


    if (oldFiber === null) {
      // If we don't have any more existing children we can choose a fast path
      // since the rest will all be insertions.
      for (; newIdx < newChildren.length; newIdx++) {
        const newFiber = createChild(returnFiber, newChildren[newIdx]);
        if (newFiber === null) {
          continue;
        }
        // lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
        if (previousNewFiber === null) {
          // TODO: Move out of the loop. This only happens for the first run.
          resultingFirstChild = newFiber;
        } else {
          previousNewFiber.sibling = newFiber;
        }
        previousNewFiber = newFiber;
      }

      return resultingFirstChild;
    }

  }

根据我们本文中所使用的模板项目的 react element type 的情况,我们可以将 createChild() 精简为下面的样子:

  function createChild(
    returnFiber,
    newChild,
  ) {
    if (
      (typeof newChild === 'string' && newChild !== '') ||
      typeof newChild === 'number'
    ) {
      // Text nodes don't have keys. If the previous node is implicitly keyed
      // we can continue to replace it without aborting even if it is not a text
      // node.
      const created = createFiberFromText(
        '' + newChild
      );
      created.return = returnFiber;
      return created;
    }

    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE: {
          const created = createFiberFromElement(
            newChild,
            returnFiber.mode,
            lanes,
          );
          created.ref = coerceRef(returnFiber, null, newChild);
          created.return = returnFiber;
          return created;
        }
        // case REACT_PORTAL_TYPE: {
        //   const created = createFiberFromPortal(
        //     newChild,
        //     returnFiber.mode,
        //     lanes,
        //   );
        //   created.return = returnFiber;
        //   return created;
        // }
        // case REACT_LAZY_TYPE: {
        //   const payload = newChild._payload;
        //   const init = newChild._init;
        //   return createChild(returnFiber, init(payload), lanes);
        // }
      }

    //   if (isArray(newChild) || getIteratorFn(newChild)) {
    //     const created = createFiberFromFragment(
    //       newChild,
    //       returnFiber.mode,
    //       lanes,
    //       null,
    //     );
    //     created.return = returnFiber;
    //     return created;
    //   }

    //   throwOnInvalidObjectType(returnFiber, newChild);
    }

    // if (__DEV__) {
    //   if (typeof newChild === 'function') {
    //     warnOnFunctionType(returnFiber);
    //   }
    // }

    // return null;
  }
  
 function createFiberFromText(
  content: string,
): Fiber {
  const HostText = 6;
  const fiber = createFiber(HostText, content, null);
  // fiber.lanes = lanes;
  return fiber;
}

我们重新将目光聚焦到 reconcileChildrenArray() 里面。通过阅读代码,我们可以看到,mount 阶段的多节点 reconciliation 本质上就是遍历由多个 react element 组成的数组,然后逐个逐个地创建新 fiber 节点,最后把它们用 sibling 指针链接起来。具体来说是这样的: 把当前迭代所创建的 fiber 节点引用赋值给上次迭代创建的 fiber 节点的sibling属性。因此,sibling 指针的指向是从左到右。最后,reconcileChildrenArray() 返回最左侧的那个新的子 fiber 节点(也即是说从左往右数数来的第一个子节点)。看起来,返回的是第一一个子 fiber 节点,实际上,返回的是一条从左到右的,由兄弟节点组成的单向链表。

回到本文所用的模板项目。假设我们当前的 fiber 节点树是这样的:

image.png

work loop 继续执行。我们需要针对 <header> 所对应 fiber 节点进行 begin work。那么,接下来进入的就是「多节点 reconciliation」流程。通过源码,我们知道,react 先根据 nextChildren 创建一个由兄弟 fiber 节点所组成的链表,如下:

image.png

注意,每个 fiber 节点创建的时候,它的 return 指针是能够确定的,所以,出现上图的情况。最后返回并赋值给 workInProgress.child 的是从左到右数来的第一个子 fiber 节点。回到本实例中,这个子节点就是 <img />。当 react 对<header> 这个 fiber 节点执行完 begin work 之后,我们得到的是这样的 fiber 树:

image.png

注意,此时的全局变量 workInProgress 指针指向哪个 fiber 节点啊?答案是:“ 所对应的 fiber 节点”

依据什么来创建新 fiber 节点呢?

上面已经讲得比较清楚,对一个 workInProgress fiber 节点进行 begin work 的目的就是创建它的子 fiber 节点。那么,这里还有一个疑点没有讲得很清晰:「给你的一个 workInProgress fiber,react 是怎么找到用于创建下一个新子 fiber 的物料呢?」这里不卖关子,这里的「物料」,其实上面也提到过,它是指「react element」。

对于 hostRootFiber 而言,它用于创建子 fiber 的物料是 <App />。它是在我们调用 ReactDOMRoot.render() 时候所传入的。随后,它被用于创建 update 对象(以下代码见于 updateContainer() 函数内):

const update = createUpdate(eventTime, lane);
  // Caution: React DevTools currently depends on this property
  // being called "element".
  update.payload = {element}; // 回到本实例,这里的 `element` 就是 `<App />`

再然后,它在updateHostRoot() 这个 begin work 阶段的 helper 函数被 processUpdateQueue() 函数处理后,成为了workInProgress.memoizedState的值。至此,它当成nextChildren的实参,被传入到 reconcileChildren() 函数里面。正式开始用于创建 fiber 子节点。

我们接着 fiber 树构建的方向看。假设当前的 workInProgerss 指针指向的是<App /> fiber 节点。那么,我们是怎么找到用于创建 <App /> fiber 节点的物料的呢?答案是:“通过调用 workInProgress.type()”。这个调用的动作具体是在 renderWithHooks()函数里面发生的。workInProgress.type() 的调用结果就是用于创建当前 workInProgress 子 fiber 节点的 nextChildren。老样子,接下来就是 reconciliation 流程,这里就不赘述了。

我们接着 fiber 树构建的方向再看,我们最后再举一个例子。假设当前的 workInProgerss 指针指向的是<div /> fiber 节点。那么,我们是怎么找到用于创建 <div /> fiber 节点的子 fiber 节点的物料的呢?答案是:“ 直接访问 workInProgress.pendingProps.children”。workInProgress.pendingProps.children 的调用结果就是用于创建 当前workInProgress 子 fiber 节点的 nextChildren。老样子,接下来就是 reconciliation 流程,这里就不赘述了。

小结

给定一个 workInProgress fiber, react 通过计算出 nextChildren 的值,并把这个值传递给 reconciliation 流程的入口函数 reconcileChildren 来创建新的子 fiber 节点。而不同类型的 fiber 计算 nextChildren 的方式不一样:

  • 对于 function component 类型的 workInProgress 来说,调用 workInProgress.type();
  • 对于 class component 类型的 workInProgress 来说,调用 workInProgress.type.render()
  • 对于 host component 类型的 workInProgress 来说,直接访问 workInProgress.pendingProps.children即可。

总结

本文意在梳理 react 应用在 mount 阶段创建初始链表的过程。这个过程由可以分为两大步骤:

  1. 第一步 - 创建链表的头部节点:ReactDOMRootfiberRootNodehostRootFiber
  2. 第二步 - 以 hostRootFiber 和 react element 树的根节点为输入,创建链表的剩余部分。

其实上面的两个步骤对应的就是我们的 react 应用入口的两行代码:

import ReactDOM from "react-dom/client";

// 第一步
const root = ReactDOM.createRoot(document.getElementById("root"));
// 第二步
root.render(<App />)

第二步骤本质上是一个「由两层嵌套 while循环 + 全局指针 workInProgress」 所实现的 work loop。在这个 work loop 中,react 构建了链表的剩余部分。work loop 可以分为先后两个阶段:

  1. begin work 阶段
  2. complete work 阶段

begin work 阶段

begin work 阶段,react 采用深度优先遍历算法自上向下地去创建全新的 fiber 节点。

  1. 找到要创建新 fiber 的「原始物料」 - nextChildren,并创建新 fiber 节点;
  2. 处理 workInProgress fiber 与新创建 fiber 之间的父子关系;
  3. 最后返回这个新的 fiber 节点作为下一个要处理的 workInProgress

创建全新子 fiber 节点的方式可以归纳为以下的一条公式:

全新子 fiber 节点 = reconcileChildren(workInProgress, nextChildren)

而不同类型的 fiber 计算 nextChildren 的方式不一样:

  • 对于 host root fiber 类型的 workInProgress 来说, nextChildren = workInProgress.memoizedState.element
  • 对于 function component 类型的 workInProgress 来说,nextChildren = workInProgress.type();
  • 对于 class component 类型的 workInProgress 来说,nextChildren = workInProgress.type.render()
  • 对于 host component 类型的 workInProgress 来说,nextChildren = workInProgress.pendingProps.children

第一个 workInProgresshostRootFiber

complete work 阶段

begin work 一旦触及 react element 树的子节点就结束了。work loop 会对当前的 workInProgress 进行 complete work。当前的 workInProgress 一旦结束了 complete work, work loop 会检查当前的 workInProgress 是否有兄弟节点,有的话,那么就会对它的兄弟节点进行新的一轮 work loop。

当子级上的所有的子 fiber 节点都走完了 complete work 阶段,那么 work loop 就会对它们的父 fiber 节点进行 complete work。以此类推,work loop 会以「先是从左到右,后是从下到上」的方向对新建的 fiber 节点进行 complete work,直至回到 hostRootFiber ,对 hostRootFiber 进行 complete work 为止。