react 构造过程
// 需要手动启用 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 阶段进行渲染
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
节点全部设
- 先去
- 第一次循环
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