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节点全部设