更新流程的目的:
- 生成wip fiberNode树
- 标记副作用flags
更新流程的步骤:
- 递:beginWork
- 归:completeWork
在 上一节 ,我们探讨了 React 应用在首次渲染或后续更新时的整体更新流程。在 Reconciler
工作流程中,beginWork
和 completeWork
两个方法起到了关键作用。beginWork
负责构建表示更新的 Fiber 树,而 completeWork
则将这个 Fiber 树映射到实际的 DOM 结构上。接下来,我们将深入实现这两个方法。
首先,在开发环境下增加 DEV
标识,以便在开发环境中方便地打印日志:
pnpm i -D -w @rollup/plugin-replace
安装完成后,在 scripts/rollup/utils.js
中引入:
// scripts/rollup/utils.js // ... import replace from '@rollup/plugin-replace' ; // ... export function getBaseRollupPlugins ( { alias = { __DEV__: true }, typescript = {} } = {} ) { return [ replace (alias), cjs (), ts (typescript)]; }
这样我们就可以在开发环境中打印日志了。
对于如下结构的reactElement:
<A><B/>
<A/>
当进入A的beginWork
时,通过对比B的current fiberNode
与B的reactElement
,生成对应wip fiberNode
beginWork
函数在向下遍历阶段执行,根据 Fiber 节点的类型(HostRoot
、HostComponent
、HostText
)分发任务给不同的处理函数,处理具体节点类型的更新逻辑:
export const beginWork = (wip: FiberNode) => {switch (wip.tag) {case HostRoot:return updateHostRoot(wip)case HostComponent:return updateHostComponent(wip)case HostText:return nulldefault:if (__DEV__) {console.warn('beginWork为实现的类型', wip)}break}
}
-
HostRoot
/** 处理根节点的更新,包括协调处理根节点的属性 以及子节点的更新逻辑 */ function updateHostRoot(wip: FiberNode) {const baseState = wip.memoizedStateconst updateQueue = wip.updateQueue as UpdateQueue<Element>const pending = updateQueue.shared.pendingupdateQueue.shared.pending = null // 清空更新链表// 1.计算状态的最新值const { memoizedState } = processUpdateQueue(baseState, pending) // 计算待更新状态的最新值wip.memoizedState = memoizedState // 更新协调后的状态最新值// 2. 创造子fiberNodeconst nextChildren = wip.memoizedState // 获取 children 属性reconcileChildren(wip, nextChildren) // 处理根节点的子节点,可能会递归调用其他协调函数;// 返回经过协调后的新的子节点链表return wip.child }
- 表示根节点,即应用的最顶层节点;
- 调用
updateHostRoot
函数,处理根节点的更新,包括协调处理根节点的属性以及子节点的更新逻辑; - 调用
reconcileChildren
函数,处理根节点的子节点,可能会递归调用其他协调函数; - 返回
workInProgress.child
表示经过协调后的新的子节点链表;
-
HostComponent
function updateHostComponent(wip: FiberNode) {const nextProps = wip.pendingProps// 创造子fiberNodeconst nextChildren = nextProps.children // 获取Dom的children属性reconcileChildren(wip, nextChildren) // 处理原生 DOM 元素的子节点更新,可能会递归调用其他协调函数;return wip.child }
- 表示原生 DOM 元素节点,例如
<div>
、<span>
等; - 调用
updateHostComponent
函数,处理原生 DOM 元素节点的更新,负责协调处理属性和子节点的更新逻辑; - 调用
reconcileChildren
函数,处理原生 DOM 元素的子节点更新; - 返回
workInProgress.child
表示经过协调后的新的子节点链表;
- 表示原生 DOM 元素节点,例如
-
HostText
- 表示文本节点,即 DOM 中的文本内容,例如
<p>123</p>
中的123
; - 调用
updateHostText
函数,协调处理文本节点的内容更新; - 返回
null
表示已经是叶子节点,没有子节点了;
- 表示文本节点,即 DOM 中的文本内容,例如
其中 reconcileChildren
函数的作用是,通过对比子节点的 current FiberNode
与 子节点的 ReactElement
,来生成子节点对应的 workInProgress FiberNode
。(current
是与视图中真实 UI 对应的 Fiber 树,workInProgress
是触发更新后正在 Reconciler 中计算的 Fiber 树。)
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {const current = wip.alternateif (current !== null) {// updatewip.child = reconcileChildFibers(wip, current?.child, children)} else {// mountwip.child = mountChildFibers(wip, null, children)}
}
reconcileChildren
函数中调用了 reconcileChildFibers
和 mountChildFibers
两个函数,它们分别负责处理更新阶段和首次渲染阶段的子节点协调。
reconcileChildFibers:
reconcileChildFibers
函数作用于组件的更新阶段,即当组件已经存在于 DOM 中,需要进行更新时。- 主要任务是协调处理当前组件实例的子节点,对比之前的子节点(
current
)和新的子节点(workInProgress
)之间的变化。 - 根据子节点的类型和 key 进行对比,决定是复用、更新、插入还是删除子节点,最终形成新的子节点链表。
mountChildFibers:
mountChildFibers
函数作用于组件的首次渲染阶段,即当组件第一次被渲染到 DOM 中时。- 主要任务是协调处理首次渲染时组件实例的子节点。
- 但此时是首次渲染,没有之前的子节点,所以主要是创建新的子节点链表。
// packages/react-reconciler/src/childFiber.ts
/** 实现生成子节点fiber 以及标记Flags的过程 */import { ReactElementType } from 'shared/ReactTypes'
import { FiberNode, createFiberFromElement } from './fiber'
import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols'
import { HostText } from './workTags'
import { Placement } from './fiberFlags'function ChildrenReconciler(shouldTrackEffects: boolean) {/** 处理单个 Element 节点的情况对比 currentFiber 与 ReactElement生成 workInProgress FiberNode */function reconcileSingleElement(returnFiber: FiberNode,currentFiber: FiberNode | null,element: ReactElementType) {// 根据element创建fiberconst fiber = createFiberFromElement(element)fiber.return = returnFiberreturn fiber}/** 处理文本节点的情况对比 currentFiber 与 ReactElement生成 workInProgress FiberNode */function reconcileSingleTextNode(returnFiber: FiberNode,currentFiber: FiberNode | null,content: string | number) {const fiber = new FiberNode(HostText, { content }, null)fiber.return = returnFiberreturn fiber}/** 为 Fiber 节点添加更新 flags */function placeSingleChild(fiber: FiberNode) {// 优化策略,首屏渲染且追踪副作用时,才添加更新 flagsif (shouldTrackEffects && fiber.alternate === null) {fiber.flags |= Placement}return fiber}// 闭包,根据 shouldTrackSideEffects 返回不同 reconcileChildFibers 的实现return function reconcileChildFibers(returnFiber: FiberNode,currentFiber: FiberNode | null,newChild?: ReactElementType) {// 判断当前fiber的类型// 1. 单个 Element 节点if (typeof newChild === 'object' && newChild !== null) {switch (newChild.$$typeof) {case REACT_ELEMENT_TYPE:return placeSingleChild(reconcileSingleElement(returnFiber, currentFiber, newChild))default:if (__DEV__) {console.warn('未实现的reconcile类型', newChild)}break}}// TODO 2. 多个 Element 节点 ul > li*3// 3. HostText 节点if (typeof newChild === 'string' || typeof newChild === 'number') {return placeSingleChild(reconcileSingleTextNode(returnFiber, currentFiber, newChild))}// 4. 其他if (__DEV__) {console.warn('未实现的reconcile类型', newChild)}return null}
}/** 处理更新阶段的子节点协调,组件的更新阶段中,追踪副作用*/
export const reconcileChildFibers = ChildrenReconciler(true)
/** 处理首次渲染阶段的子节点协调,首屏渲染阶段中不追踪副作用,只对根节点执行一次 DOM 插入操作*/
export const mountChildFibers = ChildrenReconciler(false)
// packages/react-reconciler/src/fiber.ts
// .../** 根据element创建fiber并返回 */
export function createFiberFromElement(element: ReactElementType) {const { type, key, props } = elementlet fiberTag: WorkTag = FunctionComponentif (typeof type === 'string') {// <div/> type: 'div'fiberTag = HostComponent} else if (typeof type !== 'function' && __DEV__) {console.warn('未定义的type类型', element)}// 创建 fiber 节点const fiber = new FiberNode(fiberTag, props, key)fiber.type = typereturn fiber
}
beginWork性能优化策略
考虑如下结构的reacetElement:
<div><p>练习时长</p><span>两年半</span>
</div>
理论上mount流程完毕后包含的flags:
- 两年半 Placement
- Span Placement
- 练习时长 Placement
- P Placement
- Div Placement
相比执行5次Placement,我们可以构建好离屏DOM树后,对div执行1次Placement操作
需要解决的问题:
- 对于 Host 类型 fiberNode: 构建离屏Dom树
- 标记 Update flag (TODO)
completeWork
函数在向上遍历阶段执行,根据 Fiber 节点的类型(HostRoot
、HostComponent
、HostText
等)构建 DOM 节点,收集更新 flags,并根据更新 flags 执行不同的 DOM 操作:
-
HostComponent:
- 表示原生 DOM 元素节点;
- 构建 DOM 节点,并调用
appendAllChildren
函数将 DOM 插入到 DOM 树中; - 收集更新 flags,并根据更新 flags 执行不同的 DOM 操作,例如插入新节点、更新节点属性、删除节点等;
-
HostText:
- 表示文本节点;
- 构建 DOM 节点,并将 DOM 插入到 DOM 树中;
- 收集更新 flags,根据 flags 的值,更新文本节点的内容;
-
HostRoot:
- 表示根节点;
- 会执行一些与根节点相关的最终操作,例如处理根节点的属性,确保整个应用更新完毕;
export const completeWork = (wip: FiberNode) => {const newProps = wip.pendingPropsconst current = wip.alternateswitch (wip.tag) {case HostComponent:if (current !== null && wip.stateNode) {//update} else {// mount 构建离屏的 Dom 树// 1. 构建 Domconst instance = createInstance(wip.type, newProps)// 2. 将Dom插入到Dom树中appendAllChildren(instance, wip)wip.stateNode = instance}bubbleProperties(wip)return nullcase HostText:if (current !== null && wip.stateNode) {//update} else {// mount// 1. 构建 Domconst instance = createTextInstance(newProps.content)wip.stateNode = instance}bubbleProperties(wip)return nullcase HostRoot:bubbleProperties(wip)return nulldefault:if (__DEV__) {console.warn('completeWork未实现的类型', wip)}break}
}
其中,appendAllChildren
函数负责递归地将组件的子节点添加到指定的 parent
中,它通过深度优先遍历 workInProgress
的子节点链表,处理每个子节点的类型。先处理当前节点的所有子节点,再处理兄弟节点。
如果它是原生 DOM 元素节点或文本节点,则将其添加到父节点中;如果是其他类型的组件节点并且有子节点,则递归处理其子节点。
// packages/react-reconciler/src/completeWork.ts
// ...
function appendAllChildren(parent: FiberNode, wip: FiberNode) {let node = wip.child// 递归插入while (node !== null) {if (node?.tag === HostComponent || node?.tag === HostText) {appendInitialChild(parent, node?.stateNode)} else if (node.child !== null) {node.child.return = nodenode = node.childcontinue}// 终止条件if (node === wip) {return}// 子节点结束,开始处理兄弟节点while (node.sibling === null) {// 1.当前节点无兄弟节点if (node.return === null || node.return === wip) {return}node = node?.return}// 2.当前节点有兄弟节点node.sibling.return = node.returnnode = node.sibling}
}
completeWork 性能优化策略
flags分布在不同fiberNode中,如何快速他们?
- 利用completeWork向上遍历(归)的流程,将子fiberNode的flags冒泡到父fiberNode
创建 bubbleProperties
函,负责在 completeWork
函数向上遍历的过程中,通过向上冒泡子节点的 flags,将所有更新 flags 收集到根节点。主要包含以下步骤:
- 从当前需要冒泡属性的 Fiber 节点开始,检查是否有需要冒泡的属性。
- 如果当前节点有需要冒泡的属性,将这些属性冒泡到父节点的
subtreeFlags
或其他适当的属性中。 - 递归调用
bubbleProperties
函数,处理父节点,将属性继续冒泡到更上层的祖先节点,直至达到根节点。
// packages/react-reconciler/src/completeWork.ts
// .../** 收集更新 flags,将子 FiberNode 的 flags 冒泡到父 FiberNode 上 */
function bubbleProperties(wip: FiberNode) {let subtreeFlags = NoFlagslet child = wip.childwhile (child !== null) {subtreeFlags |= child.subtreeFlagssubtreeFlags |= child.flagschild.return = wipchild = child.sibling}wip.subtreeFlags |= subtreeFlags
}
- 位运算简介
flags 是 React 中很重要的一环,具体作用是通过二进制在每个 Fiber 节点保存其本身与子节点的 flags。在保存与处理 flags 时,使用了一些二进制运算符,我们来复习一下:
1. |
运算
|
运算的两个位都为 0 时,结果才为 0:
1 | 1 = 1
1 | 0 = 1
0 | 0 = 0
React 利用了 |
运算符的特性来存储 flags,如:
const NoFlags = /* */ 0b0000000;
const PerformedWork = /* */ 0b0000001;
const Placement = /* */ 0b0000010;
const Update = /* */ 0b0000100;
const ChildDeletion = /* */ 0b0001000;const flags = Placement | Update; //此时 flags = 0b0000110
2. &
运算
&
运算的两个位都为 1 时,结果才为 1:
1 & 1 = 1
1 & 0 = 0
0 & 0 = 0
React 中会用一个 flags &
某一个 flag,来判断 flags 中是否包含某一个 flag,如:
const flags = Placement | Update; //此时 flags = 0b0000110Boolean(flags & Placement); // true, 说明 flags 中包含 Placement
Boolean(flags & ChildDeletion); // false, 说明 flags 中不包含 ChildDeletion
3. ~
运算
~
运算符会把每一位取反,0 变 1,1 变 0:
~1 = 0
~0 = 1
在 React 中,~
运算符同样是常用操作,如:
let flags = Placement | Update; //此时 flags = 0b0000110flags &= ~Placement; //此时 flags = 0b0000100
通过 ~
运算符与 &
运算符的结合,从 flags 中删除了 Placement
这个 flag。
至此,我们就实现了 React 协调阶段中的 beginWork
和 completeWork
函数,生成了一棵表示更新的 Fiber 树,并收集了树中节点的更新 flags,下一节我们将根据这些 flags 执行对应的 DOM 操作。
借鉴链接: https://juejin.cn/post/7347911786802511924