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,如果map有key,会把key从map中删除,说明可以复用,把当前节点标记为更新。新地位高的不动,新地位低的动(中间插入链表比链表屁股插入费劲)所以地位低的动动
        3. lastPlaceIndex指针,指向最后一个不需要动的老节点的key。每次新 jsx 复用到节点,lastPlaceIndex会指向老节点的最后一个成功复用的老fiber节点。如果新复用的节点 key 小于lastPlaceIndex,说明老fiber节点的顺序在新jsx之前,需要挪动位置接到新jsx节点后面。
        4. 如果jsx没有复用的老fiber,直接插入新的
        5. map中只剩还没被复用的节点,等着新的jsx数组遍历完,map里面的fiber节点全部设