react 构造过程
js
// 需要手动启用Concurrent 模式
ReactDOM.render(<App />, document.getElementById('root'), {
unstable_concurrentMode: true, // 启用 Concurrent 模式
unstable_scheduleHydrationTarget: document.getElementById('root') // 设置 hydration 目标
})
fiber 树的构造
- 除此创建:React 首次启动,页面还没有渲染,直接构造一整棵树
- 对比更新:界面已经渲染,场景新的 fiber 之前,需要与旧的 fiber 进行对象
- 深度优先遍历
- 探寻阶段 beginWork
- 根据 ReactElement 对象创建所有的 fiber 节点, 最终构造出 fiber 树形结构(设置 return 和 sibling 指针)
- 给节点打标签:设置 fiber.flags(标记 fiber 节点 的增,删,改状态, 等待 completeWork 阶段处理)
- 设置真实 DOM 的局部状态:设置 fiber.stateNode 局部状态
- 回溯阶段 completeWork
- 给 fiber 节点创建 DOM 实例, 设置 fiber.stateNode 局部状态;为 DOM 节点设置属性, 绑定事件(合成事件原理);设置 fiber.flags 标记
- 把当前 fiber 对象的副作用队列(firstEffect 和 lastEffect)添加到父节点的副作用队列之后, 更新父节点的 firstEffect 和 lastEffect 指针.
- 判断当前 fiber 是否有副作用(增,删,改), 如果有, 需要将当前 fiber 加入到父节点的 effects 队列, 等待 commit 阶段处理.
- 探寻阶段 beginWork
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 更新优化原则
- 只对同级节点进行对比,如果 DOM 节点跨层级移动,则 react 不会复用
- 不同类型的元素会产出不同的结构,会销毁老的结构,创建新的结构
- 可以通过 key 标示移动的元素
- 类型一致的节点才有继续 diff 的必要性
diff 算法介绍
- 单节点
- 如果是新增节点, 直接新建 fiber, 没有多余的逻辑
- 如果是对比更新
- 如果 key 和 type 都相同,则复用
- 否则新建
- 多节点(多节点一般会存在两轮遍历,第一轮
寻找公共序列
,第二轮遍历剩余非公共序列
)- 第一次循环
key 不同
导致不可复用,立即跳出整个遍历,第一轮遍历结束。- key 相同
type 不同
导致不可复用,会将 oldFiber 标记为DELETION
,并继续遍历- 如果
newChildren 遍历完(即
i === newChildren.length - 1)或者oldFiber 遍历完
(即 oldFiber.sibling === null),跳出遍历,第一轮遍历结束。 - let i = 0,遍历 newChildren,将
newChildren[i]
与oldFiber
比较,判断 DOM 节点是否可复用。如果可复用,i++,继续比较 newChildren[i]与oldFiber.sibling
,可以复用则继续遍历 - 如果不可复用,分两种情况:
- 如果
- 第二次循环: 遍历剩余非公共序列, 优先复用 oldFiber 序列中的节点。
- 如果 newChildren 与 oldFiber 同时遍历完,diff 结束
- 如果
newChildren没遍历完
,oldFiber 遍历完,意味着没有可以复用的节点了,遍历剩下的 newChildren 为生成的workInProgress
fiber 依次标记Placement
。 - 如果 newChildren 遍历完,
oldFiber没遍历完
,意味着有节点被删除了,需要遍历剩下的 oldFiber,依次标记Deletion
。 - 如果 newChildren 与 oldFiber
都没遍历完
- 先去
声明map数据结构
,遍历一遍老节点,把老 fiber 的 key 做映射 {元素的 key:老的 fiber 节点} - 继续遍历新
jsx
,如果map
有key
,会把key
从map
中删除,说明可以复用,把当前节点标记为更新
。新地位高的不动,新地位低的动(中间插入链表比链表屁股插入费劲)所以地位低的动动 lastPlaceIndex
指针,指向最后一个不需要动的老节点的key
。每次新 jsx 复用到节点,lastPlaceIndex
会指向老节点的最后一个成功复用的老fiber
节点。如果新复用的节点 key 小于lastPlaceIndex
,说明老fiber
节点的顺序在新jsx
之前,需要挪动位置接到新jsx
节点后面。- 如果
jsx
没有复用的老fiber
,直接插入新的 map
中只剩还没被复用的节点,等着新的jsx
数组遍历完,map
里面的fiber
节点全部设
- 先去
- 第一次循环