在探索React源码:初探React fiber一文我们提到:
React16之后,React的架构可以分成三层
- Scheduler(调度)
- Reconciler(协调)
- Renderer(渲染)
其中Reconciler(协调器)的作用是收集变化的组件,最终让Renderer(渲染器)将变化的组件渲染的页面当中。这个收集变化的组件的过程我们称为render(协调)阶段。在此阶段,React会遍历current fiber tree并将fiber节点与对应的React element进行对比(也就是我们常说的diff),构造出新的fiber tree —— workInProgress fiber tree。今天我们就来了解一下render
阶段的工作流程。
Reconciler
起作用的阶段我们称为render
阶段,Renderer
起作用的阶段我们称为commit
阶段
双缓冲机制
双缓存机制是一种在内存中构建并直接替换的技术。协调的过程中就使用了这种技术。
在React中同时存在着两棵fiber tree
。一棵是当前在屏幕上显示的dom对应的fiber tree,称为current fiber tree
,而另一棵是当触发新的更新任务时,React在内存中构建的fiber tree,称为workInProgress fiber tree
。
current fiber tree
和workInProgress fiber tree
中的fiber节点通过alternate属性进行连接。
currentFiber.alternate === workInProgressFiber;
workInProgressFiber.alternate === currentFiber;
React应用的根节点中也存在current属性,利用current属性在不同fiber tree的根节点之间进行切换的操作,就能够完成current fiber tree与workInProgress fiber tree之间的切换。
在协调阶段,React利用diff算法
,将产生update的React element
与current fiber tree
中对应的节点进行比较,并最终在内存中生成workInProgress fiber tree。随后Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。
fiber tree的遍历流程
引入fiber后,fiber tree的遍历过程:(不需要完全看懂,只需要看懂遍历的流程就好)
// 执行协调的循环
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
//shouldYield为Scheduler提供的函数, 通过 shouldYield 返回的结果判断当前是否还有可执行下一个工作单元的时间
while (workInProgress !== null && !shouldYield()) {
workInProgress = performUnitOfWork(workInProgress);
}
}
function performUnitOfWork(unitOfWork: Fiber): void {
//...
let next;
//...
//对当前节点进行协调,如果存在子节点,则返回子节点的引用
next = beginWork(current, unitOfWork, subtreeRenderLanes);
//...
//如果无子节点,则代表当前的child链表已经遍历完
if (next === null) {
// If this doesn't spawn new work, complete the current work.
//此函数内部会帮我们找到下一个可执行的节点
completeUnitOfWork(unitOfWork);
} else {
workInProgress = next;
}
//...
}
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
//...
//查看当前节点是否存在兄弟节点
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// If there is more work to do in this returnFiber, do that next.
//若存在,便把siblingFiber节点作为下一个工作单元,继续执行performUnitOfWork,执行当前节点并尝试遍历当前节点所在的child链表
workInProgress = siblingFiber;
return;
}
// Otherwise, return to the parent
//如果不存在兄弟节点,则回溯到父节点,尝试查找父节点的兄弟节点
completedWork = returnFiber;
// Update the next thing we're working on in case something throws.
workInProgress = completedWork;
} while (completedWork !== null);
//...
}
未命名文件 (2).png
这个遍历的过程实际上就是协调的整体过程,接下来我们来详细看看在新的fiber节点是如何被创建的以及新的fiber树是怎样构建出来的。
performSyncWorkOnRoot/performConcurrentWorkOnRoot
协调阶段的入口为performSyncWorkOnRoot
(legacy模式)或performConcurrentWorkOnRoot
(concurrent 模式)。
// performSyncWorkOnRoot会调用该方法
function workLoopSync() {
while (workInProgress !== null) {
performUnitOfWork(workInProgress);
}
}
// performConcurrentWorkOnRoot会调用该方法
function workLoopConcurrent() {
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
这两个方法会将生成workInProgress
的下一级的fiber
节点,并将workInProgress
的第一个子fiber
节点赋值给workInProgress
。新的workInProgress
会与已创建的fiber
节点连接起来构成workInProgress fiber tree
。
他们俩唯一的区别就是在判断是否需要继续遍历时,performConcurrentWorkOnRoot
会在判断是否存在下一工作单元workInProgress
的基础上,还会通过Scheduler
模块提供的shouldYield
方法来询问当前浏览器是否有充足的时间来执行下一工作单元。
三种链表的遍历
引入fiber前,React遍历节点的方式是n叉树的深度优先遍历,而引入fiber后,从fiber tree的遍历过程我们能够知道,React将遍历的方法从原来的n叉树的深度优先遍历改变为对多种单向链表的遍历:
- 由 fiber.child 连接的
父 -> 子
链表的遍历 - 由 fiber.return 连接的
子 -> 父
链表的遍历 - 由 fiber.sibling 连接的
兄 -> 弟
链表的遍历
这三种链表的遍历主要通过beginWork
和completeWork
两个方法进行,我们来重点分析一下这两个方法。
beginWork
beginWork的执行路径是workInProgress fiber tree
中所有的父 -> 子
链表。beginWork
会根据传入的fiber节点创建出当前workInProgress fiber
节点的所有次级workInProgress fiber
节点(这些次级节点会通过fiber.sibling
进行连接),并将当前workInProgress fiber
节点于次级的第一个workInProgress fiber
通过fiber.child
属性连接起来。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
// ...
}
beginWork接收三个参数:
-
current
:当前组件在current fiber tree
中对应的fiber
节点,即workInProgress.alternate
; -
workInProgress
:当前组件在workInProgerss fiber tree
中对应的fiber
节点,即current.alternate
; -
renderLanes
:此次render的优先级;
我们知道,current fiber tree
和workInProgress fiber tree
中的fiber节点通过alternate
属性进行连接的。
组件在mount时,由于是首次渲染,workInProgress fiber tree
中除了根节点fiberRootNode
之外,其余节点都不存在上一次更新时的fiber节点,也就是说,在mount时,workInProgress fiber tree
中除了根节点之外,所有节点的alternate
都为空。所以在mount时,除了根节点fiberRootNode
之外,其余节点调用beginWork时参数current
等于null
。
而update时,workInProgress fiber tree
所有节点都存在上一次更新时的fiber节点,所以current !== null。
beginWork在mount和update时会分别执行不同分支的工作。我们可以通过 current === null
作为条件,判断组件是处于mount还是update。随后会根据当前的workInProgress.tag
的不同,进入到不同的分支执行创建子Fiber节点的操作。
function beginWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
//...
if (current !== null) {
//update时
//...
} else {
//mount时
didReceiveUpdate = false;
}
//...
//根据tag不同,创建不同的子Fiber节点
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...省略
case LazyComponent:
// ...省略
case FunctionComponent:
// ...省略
case ClassComponent:
// ...省略
case HostRoot:
// ...省略
case HostComponent:
// ...省略
case HostText:
// ...省略
// ...省略其他类型
}
}
update时的beginWork
此时workInProgress
存在对应的current
节点,当current
和workInProgress
满足一定条件时,我们可以复用current
节点的子节点的作为workInProgress
的子节点,反之则需要进入对比(diff
)的流程,根据比对的结果创建workInProgress
的子节点。
beginWork
在创建fiber节点的过程中中会依赖一个didReceiveUpdate
变量来标识当前的current
是否有更新。
在满足下面的几种情况时,didReceiveUpdate === false:
-
未使用forceUpdate,且oldProps === newProps && workInProgress.type === current.type && !hasLegacyContextChanged() ,即props、fiber.type和context都未发生变化
-
未使用forceUpdate,且!includesSomeLane(renderLanes, updateLanes),即当前fiber节点优先级低于当前更新的优先级
const updateLanes = workInProgress.lanes;
if (current !== null) {
//update时
const oldProps = current.memoizedProps;
const newProps = workInProgress.pendingProps;
if (
oldProps !== newProps ||
hasLegacyContextChanged() ||
(__DEV__ ? workInProgress.type !== current.type : false)
) {
didReceiveUpdate = true;
} else if (!includesSomeLane(renderLanes, updateLanes)) {
// 本次的渲染优先级renderLanes不包含fiber.lanes, 表明当前fiber节点优先级低于本次的渲染优先级,不需渲染
didReceiveUpdate = false;
//...
// 虽然当前节点不需要更新,但需要使用bailoutOnAlreadyFinishedWork循环检测子节点是否需要更新
return bailoutOnAlreadyFinishedWork(current, workInProgress, renderLanes);
} else {
if ((current.effectTag & ForceUpdateForLegacySuspense) !== NoEffect) {
// forceUpdate产生的更新,需要强制渲染
didReceiveUpdate = true;
} else {
didReceiveUpdate = false;
}
}
} else {
//mount时
//...
}
mount时的beginWork
由于在mount时,直接将didReceiveUpdate赋值为false。
const updateLanes = workInProgress.lanes;
if (current !== null) {
//update时
//...
} else {
//mount时
didReceiveUpdate = false;
}
此处mount和update的不同主要体现在在didReceiveUpdate的赋值逻辑的不同, 后续进入diff阶段后,针对mount和update,diff的逻辑也会有所差别。
updateXXX
beginWork会根据当前的workInProgress.tag
的不同,进入到不同的分支执行创建子Fiber节点的操作。
switch (workInProgress.tag) {
case IndeterminateComponent:
// ...
case LazyComponent:
// ...
case FunctionComponent:
// ...
case ClassComponent:
// ...
case HostRoot:
// ...
case HostComponent:
// ...
case HostText:
// ...
// ...
}
各个分支中的updateXXX
函数的逻辑大致相同,主要经历了下面的几个步骤:
-
计算当前
workInProgress
的fiber.memoizedState
、fiber.memoizedProps
等需要持久化的数据; -
获取下级
ReactElement
对象,根据实际情况, 设置fiber.effectTag
; -
根据
ReactElement
对象, 调用reconcilerChildren
生成下级fiber
子节点,并将第一个子fiber节点赋值给workInProgress.child。同时,根据实际情况, 设置fiber.effectTag
;
我们以updateHostComponent为例进行分析。HostComponent
代表原生的 DOM 元素节点(如div
,span
,p
等节点),这些节点的更新会进入updateHostComponent。
function updateHostComponent(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
) {
//...
//1. 状态计算, 由于HostComponent是无状态组件, 所以只需要收集 nextProps即可, 它没有 memoizedState
const type = workInProgress.type;
const nextProps = workInProgress.pendingProps;
const prevProps = current !== null ? current.memoizedProps : null;
// 2. 获取下级`ReactElement`对象
let nextChildren = nextProps.children;
const isDirectTextChild = shouldSetTextContent(type, nextProps);
if (isDirectTextChild) {
// 如果子节点只有一个文本节点, 不用再创建一个HostText类型的fiber
nextChildren = null;
} else if (prevProps !== null && shouldSetTextContent(type, prevProps)) {
// 特殊操作需要设置fiber.effectTag
workInProgress.effectTag |= ContentReset;
}
// 特殊操作需要设置fiber.effectTag
markRef(current, workInProgress);
// 3. 根据`ReactElement`对象, 调用`reconcilerChildren`生成`fiber`子节点,并将第一个子fiber节点赋值给workInProgress.child。
reconcileChildren(current, workInProgress, nextChildren, renderLanes);
return workInProgress.child;
}
在各个updateXXX
函数中,会判断当前节点是否需要更新,如果不需要更新则会进入bailoutOnAlreadyFinishedWork
,并使用bailoutOnAlreadyFinishedWork
的结果作为beginWork的返回值,提前beginWork,而不需要进入diff阶段。
常见的不需要更新的情况
- updateClassComponent时若!shouldUpdate && !didCaptureError
- updateFunctionComponent时若current !== null && !didReceiveUpdate
- updateMemoComponent时若compare(prevProps, nextProps) && current.ref === workInProgress.ref
- updateHostRoot时若nextChildren === prevChildren
bailoutOnAlreadyFinishedWork
bailoutOnAlreadyFinishedWork
内部先会判断!includesSomeLane(renderLanes, workInProgress.childLanes)
是否成立。
若!includesSomeLane(renderLanes, workInProgress.childLanes)成立,则所有的子节点都不需要更新,或更新的优先级都低于当前更新的渲染优先级。此时以此节点为头节点的整颗子树都可以直接复用。此时会跳过整颗子树,并使用null作为beginWork的返回值(进入回溯的逻辑);
若不成立,则表示虽然当前节点不需要更新,但当前节点存在某些fiber子节点需要在此次渲染中进行更新,则复用current fiber生成workInProgress的次级节点;
function bailoutOnAlreadyFinishedWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
//...
if (!includesSomeLane(renderLanes, workInProgress.childLanes)) {
// renderLanes 不包含 workInProgress.childLanes
// 所有的子节点都不需要在本次更新进行更新操作,直接跳过,进行回溯
return null;
}
//...
// 虽然此节点不需要更新,此节点的某些子节点需要更新,需要继续进行协调
cloneChildFibers(current, workInProgress);
return workInProgress.child;
}
effectTag
上面我们介绍到在updateXXX
的主要逻辑中,在获取下级ReactElement
以及根据ReactElement对象, 调用reconcilerChildren
生成fiber子节点时,都会根据实际情况,进行effectTag
的设置。那么effrctTag
的作用到底是什么呢?
我们知道,Reconciler的目的之一就是负责找出变化的组件,随后通知Renderer需要执行的DOM操作,effectTag正是用于保存要执行DOM操作的具体类型的。
effectTag通过二进制表示:
//...
// 意味着该Fiber节点对应的DOM节点需要插入到页面中。
export const Placement = /* */ 0b000000000000010;
//意味着该Fiber节点需要更新。
export const Update = /* */ 0b000000000000100;
export const PlacementAndUpdate = /* */ 0b000000000000110;
//意味着该Fiber节点对应的DOM节点需要从页面中删除。
export const Deletion = /* */ 0b000000000001000;
//...
通过这种方式保存effectTag可以方便的使用位操作为fiber赋值多个effect以及判断当前fiber是否存在某种effect。
React 的优先级
lane
模型中同样使用了二进制的方式来表示优先级。
reconcileChildren
在各个updateXXX
函数中,会根据获取到的下级ReactElement对象, 调用reconcilerChildren生成当前workInProgress fiber节点的下级fiber子节点。
在双缓冲机制中我们介绍到:
在协调阶段,React利用
diff算法
,将产生update的ReactElement
与current fiber tree
中对应的节点进行比较,并最终在内存中生成workInProgress fiber tree
。随后Renderer会依据workInProgress fiber tree将update渲染到页面上。同时根节点的current属性会指向workInProgress fiber tree,此时workInProgress fiber tree就变为current fiber tree。
diff的过程就是在reconcileChildren中发生的。
本文的重点是Reconciler进行协调的过程,我们只需要了解reconcileChildren函数的目的,不会对reconcileChildren中的
diff
算法的实现做更深入的了解,对React的diff
算法感兴趣的同学可阅读探索React源码:React Diff。
reconcileChildren也会通过current === null 区分mount与update,再分别执行不同的工作:
export function reconcileChildren(
current: Fiber | null,
workInProgress: Fiber,
nextChildren: any,
renderLanes: Lanes
) {
if (current === null) {
// 对于mount的组件
workInProgress.child = mountChildFibers(
workInProgress,
null,
nextChildren,
renderLanes,
);
} else {
// 对于update的组件
workInProgress.child = reconcileChildFibers(
workInProgress,
current.child,
nextChildren,
renderLanes,
);
}
}
mountChildFibers
与reconcileChildFibers
的都是通过ChildReconciler生成的。他们的不同点在于shouldTrackSideEffects
参数的不同,当shouldTrackSideEffects
为true时会为生成的fiber节点收集effectTag
属性,反之不会进行收集effectTag
属性。
这样做的目的是提升
commit
阶段的效率。如果mountChildFibers也会赋值effectTag,由于mountChildFibers的节点都是首次渲染的,所以他们的effectTag都会收集到Placement effectTag
。那么commit阶段在执行DOM操作时,会导致每个fiber节点都需要进行插入操作。为了解决这个问题,在mount时只有根节点会进行effectTag的收集,在commit阶段只会执行一次插入操作。
export const reconcileChildFibers = ChildReconciler(true);
export const mountChildFibers = ChildReconciler(false);
function ChildReconciler(shouldTrackSideEffects) {
//...
function reconcileChildrenArray(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChildren: Array<*>,
lanes: Lanes,
): Fiber | null {
//...
}
//...
function reconcileSingleElement(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
element: ReactElement,
lanes: Lanes,
): Fiber {
//...
}
//...
function reconcileChildFibers(
returnFiber: Fiber,
currentFirstChild: Fiber | null,
newChild: any,
lanes: Lanes,
): Fiber | null {
//...
}
return reconcileChildFibers;
}
ChildReconciler
内部定义了许多用于操作fiber
节点的函数,并最终会使用一个名为 reconcileChildFibers
的函数作为返回值。这个函数的主要目的是生成当前workInProgress fiber
节点的下级fiber
节点,并将第一个子fiber节点
作为本次beginWork
返回值。
reconcileChildFibers的执行过程中除了向下生成子节点之外,还会进行下列的操作:
- 把即将要在
commit
阶段中要对dom节点进行的操作(如新增,移动:Placement
, 删除:Deletion
)收集到effectTag
中; - 对于被删除的
fiber
节点, 除了节点自身的effectTag需要收集Deletion
之外, 还要将其添加到父节点的effectList
中(正常effectList的收集是在completeWork
中进行的, 但是被删除的节点会脱离fiber
树, 无法进入completeWork
的流程, 所以在beginWork
阶段提前加入父节点的effectList
)。
在遍历的流程中我们可以看到,
beginWork
返回值不为空时,会把该值赋值给workInProgress
,作为下一次的工作单元,即完成了父 -> 子
链表中的一个节点的遍历。beginWork
返回值为空时我们将进入completeWork
。
completeUnitOfWork
当beginWork
返回值为空时,代表在遍历父->子
链表的过程中发现当前链表已经无下一个节点了(也就是已遍历完当前父->子
链表),此时会进入到completeUnitOfWork
函数。
completeUnitOfWork
主要做了以下几件事情:
-
调用
completeWork
。 -
用于进行父节点的
effectList
的收集:- 把当前 fiber 节点的
effectList
合并到父节点的effectList
中。 - 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的
effectList
中。
- 把当前 fiber 节点的
-
沿着此节点所在的
兄 -> 弟
链表查看其是否拥有兄弟fiber节点(即fiber.sibling !== null),如果存在,则进入其兄弟fiber父 -> 子
链表的遍历(即进入其兄弟节点的beginWork
阶段)。如果不存在兄弟fiber,会通过子 -> 父
链表回溯到父节点上,直到回溯到根节点,也即完成本次协调。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
// 此循环控制fiber节点向父节点回溯
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.flags & Incomplete) === NoFlags) {
let next;
// 使用completeWork处理Fiber节点,后面再详细分析completeWork
next = completeWork(current, completedWork, subtreeRenderLanes); // 处理单个节点
if (next !== null) {
// Suspense类型的组件可能回派生出其他节点, 此时回到`beginWork`阶段进行处理此节点
workInProgress = next;
return;
}
// 重置子节点的优先级
resetChildLanes(completedWork);
if (
returnFiber !== null &&
(returnFiber.flags & Incomplete) === NoFlags
) {
// 将此节点的effectList合并到到父节点的effectList中
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的`effectList`中。
const flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
}
} else {
// 异常处理
//...
}
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
// 如果有兄弟节点, 则将兄弟节点作为下一个工作单元,进入到兄弟节点的beginWork阶段
workInProgress = siblingFiber;
return;
}
// 若不存在兄弟节点,则回溯到父节点
completedWork = returnFiber;
workInProgress = completedWork;
} while (completedWork !== null);
// 已回溯到根节点, 设置workInProgressRootExitStatus = RootCompleted
if (workInProgressRootExitStatus === RootIncomplete) {
workInProgressRootExitStatus = RootCompleted;
}
}
completeWork
completeWork
的作用包括:
-
为新增的 fiber 节点生成对应的DOM节点。
-
更新DOM节点的属性。
-
进行事件绑定。
-
收集effectTag。
与beginWork
类似,completeWork
针对不同fiber.tag也会进入到不同的逻辑处理分支。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
case IndeterminateComponent:
case LazyComponent:
case SimpleMemoComponent:
case FunctionComponent:
case ForwardRef:
case Fragment:
case Mode:
case Profiler:
case ContextConsumer:
case MemoComponent:
return null;
case ClassComponent: {
// ...
return null;
}
case HostRoot: {
// ...
return null;
}
case HostComponent: {
// ...
return null;
}
// ...
}
我们继续以HostComponent
类型的节点为例,进行分析。
在处理HostComponent时,我们同样需要区分当前节点是需要进行新建
操作还是更新
操作。但与beginWork
阶段判断mount还是update不同的是,判断节点是否需要更新
时,除了要满足 current !== null
之外,我们还需要考虑workInProgress.stateNode节点是否为null,只有当current !== null && workInProgress.stateNode != null
时,我们才会进行更新
操作。
个人猜测,待验证:beginWork阶段mount的节点的stateNode属性为空,并且进入到了completeWork阶段才会被赋值。若在
该节点进入到beginWork阶段之后,进入到completeWork阶段前
的这段时间内,出现了更高优先级的更新中断了此次更新的情况,就有可能出现current !== null
,但workInProgress.stateNode == null
的情况,此时需要进行新建
操作。
更新时
进入更新逻辑的fiber节点的stateNode属性不为空,即已经存在对应的DOM节点。这时候我们只需要更新DOM节点的属性并进行相关effectTag的收集。
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
// ref更新时,收集Ref effectTag
if (current.ref !== workInProgress.ref) {
markRef(workInProgress);
}
}
updateHostComponent
updateHostComponent用于更新DOM节点的属性并在当前节点存在更新属性,收集Update effectTag。
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
// props没有变化,跳过对当前节点的处理
const oldProps = current.memoizedProps;
if (oldProps === newProps) {
return;
}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// 计算需要变化的DOM节点属性,并存储到updatePayload 中,updatePayload 为一个偶数索引的值为变化的prop key,奇数索引的值为变化的prop value的数组。
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// 将updatePayload挂载到workInProgress.updateQueue上,供后续commit阶段使用
workInProgress.updateQueue = (updatePayload: any);
// 若updatePayload不为空,即当前节点存在更新属性,收集Update effectTag
if (updatePayload) {
markUpdate(workInProgress);
}
};
我们可以看到,需要变化的prop会被存储到updatePayload 中,updatePayload 为一个偶数索引的值为变化的prop key,奇数索引的值为变化的prop value的数组。并最终挂载到挂载到workInProgress.updateQueue上,供后续commit阶段使用。
prepareUpdate
prepareUpdate内部会调用diff方法用于计算updatePayload。
export function prepareUpdate(
instance: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): null | Object {
const viewConfig = instance.canonical.viewConfig;
const updatePayload = diff(oldProps, newProps, viewConfig.validAttributes);
instance.canonical.currentProps = newProps;
return updatePayload;
}
diff方法内部实际是通过diffProperties方法实现的,diffProperties会对lastProps
和nextProps
进行对比:
-
对 input/option/select/textarea 的 lastProps & nextProps 做特殊处理,此处和React受控组件的相关,不做展开。
-
遍历 lastProps:
- 当遍历到的prop属性在 nextProps 中也存在时,那么跳出本次循环(continue)。若遍历到的prop属性在 nextProps 中不存在,则进入下一步。
- 特殊处理style,判断当前prop是否为 style prop ,若不是,进入下一步,若是,则将 style prop 整理到styleUpdates中,其中styleUpdates为以style prop的key值为key,''(空字符串)为value的对象,用于清空style属性。
- 由于进入到此步骤的prop在 nextProps 中不存在,将此类型的prop整理进updatePayload,并赋值为null,表示删除此属性。
-
遍历 nextProps:
- 当遍历到的prop属性 与 lastProp 相等,即更新前后没有发生变化,跳过。
- 特殊处理style,判断当前prop是否为 style prop ,若不是,进入下一步,若是,整理到 styleUpdates 变量中,其中styleUpdates为以style prop的key值为key,tyle prop的 value 为value的对象,用于更新style属性。
- 特殊处理 DANGEROUSLY_SET_INNER_HTML
- 特殊处理 children
- 若以上场景都没命中,直接把 prop 的 key 和值都整理到updatePayload中。
- 若 styleUpdates 不为空,则将styleUpdates作为style prop 的值整理到updatePayload中。
新建时
进入新建逻辑的fiber节点的stateNode属性为空,不存在对应的DOM节点。相比于更新操作,我们需要做更多的事情:
-
为 fiber 节点生成对应的 DOM 节点,并赋值给stateNode属性。
-
将子孙DOM节点插入刚生成的DOM节点中。
-
处理 DOM 节点的所有属性以及事件回调。
-
收集effectTag。
if (current !== null && workInProgress.stateNode != null) {
// 更新操作
// ...
} else {
// 新建操作
// 创建DOM节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
// 将子孙DOM节点插入刚生成的DOM节点中
appendAllChildren(instance, workInProgress, false, false);
// 将DOM节点赋值给stateNode属性
workInProgress.stateNode = instance;
// 处理 DOM 节点的所有属性以及事件回调
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
markUpdate(workInProgress);
}
}
createInstance
createInstance负责给fiber节点生成对应的DOM节点。
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
// ...
// 创建 DOM 元素
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
// 在DOM节点中挂载一个指向 fiber 节点对象的指针
precacheFiberNode(internalInstanceHandle, domElement);
// 在 DOM节点中挂载一个指向 props 的指针
updateFiberProps(domElement, props);
return domElement;
}
appendAllChildren
appendAllChildren负责将子孙DOM节点插入刚生成的DOM节点中。
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// 获取workInProgress的子fiber节点
let node = workInProgress.child;
// 当存在子节点时,去往下遍历
while (node !== null) {
if (node.tag === HostComponent || node.tag === HostText) {
// 当node节点为HostComponent后HostText时,直接插入到子DOM节点列表的尾部
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {
// 当node节点为HostPortal类型的节点,什么都不做
} else if (node.child !== null) {
// 上面分支都没有命中,说明node节点不存在对应DOM,向下查找拥有stateNode属性的子节点
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {
// 回溯到workInProgress时,以添加完所有子节点
return;
}
// 当node节点不存在兄弟节点时,向上回溯
while (node.sibling === null) {
// 回溯到workInProgress时,以添加完所有子节点
if (node.return === null || node.return === workInProgress) {
return;
}
node = node.return;
}
// 此时workInProgress的第一个子DOM节点已经插入到进入workInProgress对应的DOM节点了,开始进入node节点的兄弟节点的插入操作
node.sibling.return = node.return;
node = node.sibling;
}
};
function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void {
parentInstance.appendChild(child);
}
我们在介绍beginWork时介绍过,在mount时,为了避免每个fiber节点都需要进行插入操作,在mount时,只有根节点会收集effectTag,其余节点不会进行effectTag的收集。由于每次执行appendAllChildren后,我们都能得到一棵以当前workInProgress为根节点的DOM树。因此在commit阶段我们只需要对mount的根节点进行一次插入操作就可以了。
finalizeInitialChildren
function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {
// 此方法会将 DOM 属性挂载到 DOM 节点上,并进行事件绑定
setInitialProperties(domElement, type, props, rootContainerInstance);
// 返回 props.autoFocus 的值
return shouldAutoFocusHostComponent(type, props);
}
effectList
我们在介绍completeUnitOfWork
函数的时候提到,他的其中一个作用是用于进行父节点的effectList
的收集:
- 把当前 fiber 节点的 effectList
合并到父节点的effectList
中。
- 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的effectList
中。
// 将此节点的effectList合并到到父节点的effectList中
if (returnFiber.firstEffect === null) {
returnFiber.firstEffect = completedWork.firstEffect;
}
if (completedWork.lastEffect !== null) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork.firstEffect;
}
returnFiber.lastEffect = completedWork.lastEffect;
}
// 若当前 fiber 节点存在存在副作用(增,删,改), 则将其加入到父节点的`effectList`中。
const flags = completedWork.flags;
if (flags > PerformedWork) {
if (returnFiber.lastEffect !== null) {
returnFiber.lastEffect.nextEffect = completedWork;
} else {
returnFiber.firstEffect = completedWork;
}
returnFiber.lastEffect = completedWork;
}
effectList是一条用于收集存在effectTag的fiber节点的单向链表。React使用fiber.firstEffect表示挂载到此fiber节点的effectList的第一个fiber节点,使用fiber.lastEffect表示挂载到此fiber节点的effectList的最后一个fiber节点。
effectList存在的目的是为了提升commit
阶段的工作效率。在commit阶段,我们需要找出所有存在effectTag的fiber节点并依次执行effectTag对应操作。为了避免在commit阶段再去做遍历操作去寻找effectTag不为空的fiber节点,React在completeUnitOfWork
函数调用的过程中提前把所有存在effectTag的节点收集到effectList中,在commit
阶段,只需要遍历effectList,并执行各个节点的effectTag的对应操作就好。
render阶段结束
在completeUnitOfWork
的回溯过程中,如果completedWork === null,说明workInProgress fiber tree
中的所有节点都已完成了completeWork
,workInProgress fiber tree
已经构建完成,至此,render阶段全部工作完成。
后续我们将回到协调阶段的入口函数performSyncWorkOnRoot
(legacy模式)或performConcurrentWorkOnRoot
(concurrent 模式)中,调用commitRoot(root)
(其中root为fiberRootNode)来开启commit
阶段的工作流程。
网友评论