深圳幻海软件技术有限公司 欢迎您!

理解 React 的调和器 Reconciler

2023-02-28

大家好,我是前端西瓜哥。今天来学习React中的调和器Reconciler。React的版本为18.2.0ReactElement、fiber和DomReactElement就是React.createElement()方法的返回结果,一种 映射真实DOM层级关系的对象,但里面可以带上组件

大家好,我是前端西瓜哥。今天来学习 React 中的调和器 Reconciler。

React 的版本为 18.2.0

ReactElement、fiber 和 Dom

ReactElement 就是 React.createElement() 方法的返回结果,一种 映射真实 DOM 层级关系的对象,但里面可以带上组件元素。通常我们会用 JSX 去写。

类组件的 render 方法的返回值和函数组件的返回值都是 ReactElement。

fiber 是一个节点,是 React Fiber 时间分片架构中的一个节点。fiber 是 ReactElement 和真实 DOM 的桥梁。

fiber 会根据 ReactElement 构建成一棵树。当更新时,组件会调用 render 方法生成新的 ReactElement,然后我们再构建一个新的 Fiber 树,和旧树对比。

fiber 树下的 fiber 节点通过下面三个字段进行关联:

  • return:父 fiber。
  • child:子 fiber 的第一个。
  • sibling :下一个兄弟节点。

一个 App 组件的 fiber 树结构:

App 的 child 是会指向它创建的 element 对应 fiber 的根节点。

构建 Fiber 树的过程

在调用 createRoot(并发模式)或者 ReactDOM.render(同步模式)时,会执行 createFiberRoot 方法。

function createFiberRoot(containerInfo, tag) {
  // 生成 fiberRoot。containerInfo 是挂载根节点(比如 div#root)
  const root = new FiberRootNode(containerInfo, tag);
  // 生成 rootFiber
  const uninitializedFiber = createHostRootFiber(tag);
  // 互相关联
  root.current = uninitializedFiber;
  uninitializedFiber.stateNode = root;

  return root;
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.

createFiberRoot 创建一个了 fiberRoot,以及一个 rootFiber。它们的关系如下:

一个 fiberRoot 是 fiber 树的根节点的维护者,是 fiberRootNode 实例,通过 current 确定要使用哪一棵 fiber 树。

每调用 createRoot 都会构建新的 fiber 树,并生成一个新的 fiberRoot 去指向它。

rootFiber 是一颗 fiber 树的根节点,也属于是 fiber 节点。rootFiber 通过属性 stateNode 访问到 fiberRoot。注意不是 return,因为 fiberRoot 的结构完全不一样,不是 fiber 节点。

creatRoot 方法返回的是个对象,它的 _internalRoot 属性指向的是这个 fiberRoot。

performUnitOfWork

在 workLoopSync 或 workLoopConcurrent 中,是会不断地调用  performUnitOfWork(workInProgress) 的。

这个函数会不断地处理以 fiber 为单位的任务。

workLoopConcurrent 的实现:

function workLoopConcurrent() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWork(workInProgress);
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.

看看 performUnitOfWork 的核心实现:

function performUnitOfWork(unitOfWork) {
  // 拿到当前 fiber 节点的 “替身”
  var current = unitOfWork.alternate; 
  var next = beginWork(current, unitOfWork, subtreeRenderLanes); // 递
  if (next === null) {
    completeUnitOfWork(unitOfWork); // 归
  } else {
    workInProgress = next;
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.

performUnitOfWork 由两部分组成:

  1. beginWork:自上而下的调和,主要在构建 fiber。fiber 其实是对之前架构 “递归” 的模拟,这个 beginWork 对应着 “递”。
  2. completeUnitOfWork:自下而上的合并阶段。对应递归的 “归”。

还是这个 fiber 树的图,这里的粉色的 1、2、4 表示的是 beginWork,3、5 则代表 completeUnitOfWork。

beginWork

performUnitOfWork 中,先调用 beginWork。

beginWork 的作用是,自上而下根据组件进行组件实例化,根据新的 element 构建 fiber,给旧的 fiber 打上 effectTag。

function beginWork(current, workInProgress, renderLanes) {
  if (current !== null) {
    const oldProps = current.memoizedProps;
    const newProps = workInProgress.pendingProps;
  }
  
  // 根据不同类型的组件,进行不同的操作
  switch (workInProgress.tag) {
    // ...
 
    case ClassComponent: {
      const Component = workInProgress.type;
      // 未处理的新 props 对象
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      // 更新组件
      return updateClassComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
    
    // ...
  }
}
  • 1.
  • 2.
  • 3.
  • 4.
  • 5.
  • 6.
  • 7.
  • 8.
  • 9.
  • 10.
  • 11.
  • 12.
  • 13.
  • 14.
  • 15.
  • 16.
  • 17.
  • 18.
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 30.
  • 31.

current 是旧节点,workInProgress 是新节点,属于半成品,会在执行过程中一点点填充内容。

beginWork 会进行深度优先遍历,生成的新节点 workInProcess 会 tag 属性进入不同的 update 逻辑分支,比如常见的 updateHostRoot、updateClassComponent、updateFunctionComponent 等。

  1. 会做组件实例化,拿到新 ReactElement,然后调用 reconcileChildFibers 方法进行 新旧节点 diff,深度递归构建子 fiber,形成子 fiber 树,并把一些数据持久化挂载到 fiber 上。
  2. 在这个过程中,会给 workInProcess 打标记。具体是在 fiber.flags 上标记  Placement(插入)、Update(更新)等 flags。比如在同层级进行对比,发现旧节点有需要删除的 fiber,这些 fiber 会放到新父 fiber 的 deletions 数组中,并加上 ChildDetetion 标签。
  3. 这期间穿插调用了一些生命周期函数(比如 shouldComponentUpdate),都是是在新旧节点对比前,准确来说是调用 render 拿到 ReactElement 的时机之前。

completeUnitOfWork

performUnitOfWork 中,调用完 beginWork 后,会返回一个 next。这个 next 就是下一个要处理的 fiber,是 unitOfWork 的子 fiber。

这个 next 会赋值给 workInProgress,然后 workLoopConcurrent 的循环会 处理这个新的 workInProcess。

但当 next 为 null,就表示找不到下一个 fiber 了,深度的 “递” 到底了,就要 调用 completeUnitOfWork,进行 “归” 的收尾工作。

completeUnitOfWork 做的主要工作有:

  1. 根据 fiber 的类型不同,进行不同的处理。对于原生组件类型(比如 div、span)的挂载阶段,会用 createInstance 创建 一个真实 DOM,这个 DOM 下还没有子节点,然后还会将其下所有子 fiber 对应的真实 DOM 加到这个 DOM 下,然后赋值给 fiber.stateNode,此时这个 DOM 元素目前什么属性都没有。
  2. 根据 props,调用 setInitialProperties 方法绑定合成事件,以及设置 DOM 属性。

React 16 时会生成一个 effectList 来记录需要更新的节点,防止不必要的遍历整棵树。但 React 17 后被移除掉了,改成从 rootFiber 开始从上往下遍历。

结尾

调和阶段,主要分为 beginWork 和 completeUnitOfWork 两部分。

beginWork 自上而下,进行新旧节点对比,构造子 fiber,并打上 flag(插入、更新、删除),会执行 render(生成新 ReactElement) 之前的生命周期函数。对应以前 stack reconciler 架构中递归的 “递”。

completeUnitOfWork 自下而上,如果是插入,则构建真实 DOM 节点放到 fiber.stateNode 下,接着是处理 props,将属性添加到 DOM 上。