Skip to content
大纲

react 构造过程

js
// 需要手动启用 Concurrent 模式
ReactDOM.render(<App />, document.getElementById('root'), {
  unstable_concurrentMode: true, // 启用 Concurrent 模式
  unstable_scheduleHydrationTarget: document.getElementById('root') // 设置 hydration 目标
})

fiber 树的构造

  1. 除此创建:React 首次启动,页面还没有渲染,直接构造一整棵树
  2. 对比更新:界面已经渲染,场景新的 fiber 之前,需要与旧的 fiber 进行对象
  3. 深度优先遍历
    1. 探寻阶段 beginWork
      1. 根据 ReactElement 对象创建所有的 fiber 节点,最终构造出 fiber 树形结构 (设置 return 和 sibling 指针)
      2. 给节点打标签:设置 fiber.flags(标记 fiber 节点 的增,删,改状态,等待 completeWork 阶段处理)
      3. 设置真实 DOM 的局部状态:设置 fiber.stateNode 局部状态
    2. 回溯阶段 completeWork
      1. 给 fiber 节点创建 DOM 实例,设置 fiber.stateNode 局部状态;为 DOM 节点设置属性,绑定事件 (合成事件原理);设置 fiber.flags 标记
      2. 把当前 fiber 对象的副作用队列 (firstEffect 和 lastEffect) 添加到父节点的副作用队列之后,更新父节点的 firstEffect 和 lastEffect 指针。
      3. 判断当前 fiber 是否有副作用 (增,删,改), 如果有,需要将当前 fiber 加入到父节点的 effects 队列,等待 commit 阶段处理。

fiber树构造循环负责构造新的 fiber 树,构造过程中同时标记 fiber.flags, 最终把所有被标记的 fiber 节点收集到一个副作用队列中, 这个副作用队列被挂载到根节点上 (HostRootFiber.alternate.firstEffect). 此时的 fiber 树和与之对应的 DOM 节点都还在内存当中,等待 commitRoot 阶段进行渲染

js
fiber = {
  return,
  sibling,
  next,
  tag, // HostComponent, HostText 创建 DOM 实例,设置 fiber.stateNode 局部状态
  flags, // 用来标记 fiber 的增,删,改状态,在 complateWork 阶段时使用
  stateNode, // 真实 dom 的局部
  firstEffect, // 副作用队列
  lastEffect,
}

fiber 更新优化原则

  1. 只对同级节点进行对比,如果 DOM 节点跨层级移动,则 react 不会复用
  2. 不同类型的元素会产出不同的结构,会销毁老的结构,创建新的结构
  3. 可以通过 key 标示移动的元素
  4. 类型一致的节点才有继续 diff 的必要性

diff 算法介绍

  1. 单节点
    1. 如果是新增节点,直接新建 fiber, 没有多余的逻辑
    2. 如果是对比更新
      1. 如果 key 和 type 都相同,则复用
      2. 否则新建
  2. 多节点 (多节点一般会存在两轮遍历,第一轮寻找公共序列,第二轮遍历剩余非公共序列)
    1. 第一次循环
      1. key 不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。
      2. key 相同 type 不同导致不可复用,会将 oldFiber 标记为 DELETION,并继续遍历
        1. 如果 newChildren 遍历完(即 i === newChildren.length - 1)或者 oldFiber 遍历完(即 oldFiber.sibling === null),跳出遍历,第一轮遍历结束。
        2. let i = 0,遍历 newChildren,将 newChildren[i]oldFiber 比较,判断 DOM 节点是否可复用。如果可复用,i++,继续比较 newChildren[i]与 oldFiber.sibling,可以复用则继续遍历
        3. 如果不可复用,分两种情况:
    2. 第二次循环:遍历剩余非公共序列,优先复用 oldFiber 序列中的节点。
      1. 如果 newChildren 与 oldFiber 同时遍历完,diff 结束
      2. 如果 newChildren没遍历完,oldFiber 遍历完,意味着没有可以复用的节点了,遍历剩下的 newChildren 为生成的workInProgress fiber 依次标记Placement
      3. 如果 newChildren 遍历完,oldFiber没遍历完,意味着有节点被删除了,需要遍历剩下的 oldFiber,依次标记Deletion
      4. 如果 newChildren 与 oldFiber 都没遍历完
        1. 先去声明map数据结构,遍历一遍老节点,把老 fiber 的 key 做映射 {元素的 key:老的 fiber 节点}
        2. 继续遍历新jsx,如果mapkey,会把keymap中删除,说明可以复用,把当前节点标记为更新。新地位高的不动,新地位低的动(中间插入链表比链表屁股插入费劲)所以地位低的动动
        3. lastPlaceIndex指针,指向最后一个不需要动的老节点的key。每次新 jsx 复用到节点,lastPlaceIndex会指向老节点的最后一个成功复用的老fiber节点。如果新复用的节点 key 小于lastPlaceIndex,说明老fiber节点的顺序在新jsx之前,需要挪动位置接到新jsx节点后面。
        4. 如果jsx没有复用的老fiber,直接插入新的
        5. map中只剩还没被复用的节点,等着新的jsx数组遍历完,map里面的fiber节点全部设

Hook 分为了 2 个类别,状态 Hook, 和副作用 Hook

状态 Hook 广义上讲:能实现数据持久化且没有副作用的 Hook, 均可以视为状态 Hoo,所以还包括 useContext, useRef, useCallback, useMemo 等。这类 Hook 内部没有使用 useState/useReducer, 但是它们也能实现多次 render 时,保持其初始值不变 (即数据持久化) 且没有任何副作用。 得益于双缓冲技术 (double buffering), 在多次 render 时,以 fiber 为载体,保证复用同一个 Hook 对象,进而实现数据持久化。

副作用 Hook 状态 Hook 实现了状态持久化,而副作用 Hook 则会修改 fiber.flags. (在 performUnitOfWork->completeWork 阶段,所有存在副作用的 fiber 节点,都会被添加到父节点的副作用队列后, 最后在 commitRoot 阶段处理这些副作用节点).要实现副作用,必须直接或间接的调用 useEffect.

组合 Hook react 内部有 useDeferredValue, useTransition, useMutableSource, useOpaqueIdentifier, 自定义 Hook

'useState' | 'useReducer' | 'useContext' | 'useRef' | 'useEffect' | 'useLayoutEffect' | 'useCallback' | 'useMemo' | 'useImperativeHandle' | 'useDebugValue' | 'useDeferredValue' | 'useTransition' | 'useMutableSource' | 'useOpaqueIdentifier';

从 fiber 树构造的视角来看,不同的 fiber 类型,只需要调用不同的处理函数返回 fiber 子节点。所以在 performUnitOfWork->beginWork 函数中,调用了多种处理函数。从调用方来讲,无需关心处理函数的内部实现 (比如 updateFunctionComponent 内部使用了 Hook 对象,updateClassComponent 内部使用了 class 实例) 在 updateFunctionComponent 函数中调用了 renderWithHooks(位于 ReactFiberHooks) , 至此 Fiber 与 Hook 产生了关联 reconcileChildren 进入 reconcile 函数,生成下级 fiber 节点

执行上下文

executionContext, 代表渲染期间的执行栈,是其在 scheduleUpdateOnFiber 判断里操控 reconciler 运作流程,每一个阶段都会改变 executionContext export const NoContext = 0b0000000; const BatchedContext = 0b0000001; const EventContext = 0b0000010; const DiscreteEventContext = 0b0000100; const LegacyUnbatchedContext = 0b0001000; const RenderContext = 0b0010000; const CommitContext = 0b0100000;

双缓冲技术 (double buffering) 在 ReactElement 转换成 fiber 树的过程中,内存里会同时存在 2 棵 fiber 树:

  • 代表当前界面的 fiber 树,挂载到 fiberRoot.current 上
  • 正在构造的 fiber 树,挂载到 HostRootFiber.alternate 上,正在构造的节点称为 workInProgress,当构造完成之后,重新渲染页面,最后切换 fiberRoot.current = workInProgress
    • fiberRoot.current 指向当前界面对应的 fiber 树。

优先级

  • update 优先级
    • 有 2 种情况会创建 update 对象:应用初始化:在 react-reconciler 包中的 updateContainer 函数中 和 发起组件更新:假设在 class 组件中调用 setState