系列文章目录(同步更新)
- React 源码解析系列 – React 的 render 阶段(一):根本流程介绍
- React 源码解析系列 – React 的 render 阶段(二):beginWork
- React 源码解析系列 – React 的 render 阶段(三):completeUnitOfWork
本系列文章均为探讨 React v17.0.0-alpha 的源码
performUnitOfWork
回顾《React 源码解析系列 – React 的 render 阶段(一):根本流程介绍》中介绍的 performUnitOfWork 办法:
function performUnitOfWork(unitOfWork: Fiber): void {
const current = unitOfWork.alternate; // current 树上对应的 Fiber 节点,有可能为 null
// ... 省略
let next; // 用来寄存 beginWork()返回的后果
next = beginWork(current, unitOfWork, subtreeRenderLanes);
// ... 省略
unitOfWork.memoizedProps = unitOfWork.pendingProps;
if (next === null) { // beginWork 返回 null,示意无(或无需关注)以后节点的子 Fiber 节点
completeUnitOfWork(unitOfWork);
} else {workInProgress = next; // 下次的 workLoopSync/workLoopConcurrent 的 while 循环的循环主体为子 Fiber 节点}
// ... 省略
}
作为 render 的“归”阶段,需在 render 的“递”阶段完结后才会执行;换句话说,当 beginWork 返回 null 值,即 以后节点无(或无需关注)以后节点的子 Fiber 节点 时,才会进入到 render 的“归”阶段 —— completeUnitOfWork。
completeUnitOfWork
上面来看本文的配角 —— completeUnitOfWork 办法:
function completeUnitOfWork(unitOfWork: Fiber): void {
/*
实现对以后 Fiber 节点的一些解决
解决实现后,若以后节点尚有 sibling 节点,则完结以后办法,进入到下一次的 performUnitOfWork 的循环中
若已没有 sibling 节点,则回退解决父节点(completedWork.return),直到父节点为 null,示意整棵 workInProgress fiber 树已处理完毕。*/
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.effectTag & Incomplete) === NoEffect) {
let next;
// ... 省略
next = completeWork(current, completedWork, subtreeRenderLanes);
// ... 省略
/*
如果 completeWork 返回不为空,则进入到下一次的 performUnitOfWork 循环中
但这种状况太常见,目前我只看到 Suspense 相干会有返回,因而此代码段权且认为不会执行
*/
if (next !== null) {
workInProgress = next;
return;
}
// ... 省略
if (
returnFiber !== null &&
(returnFiber.effectTag & Incomplete) === NoEffect
) {/* 收集所有带有 EffectTag 的子 Fiber 节点,以链表 (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 节点 (completedWork) 也有 EffectTag,那么将其放在 (EffectList 中) 子 Fiber 节点前面 */
const effectTag = completedWork.effectTag;
/* 跳过 NoWork/PerformedWork 这两种 EffectTag 的节点,NoWork 就不必解释了,PerformedWork 是给 DevTools 用的 */
if (effectTag > PerformedWork) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}
returnFiber.lastEffect = completedWork;
}
}
} else {// 异样解决,省略...}
// 取以后 Fiber 节点 (completedWork) 的兄弟 (sibling) 节点;// 如果有值,则完结 completeUnitOfWork,并将该兄弟节点作为下次 performUnitOfWork 的主体(unitOfWork)
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
// 若没有兄弟节点,则将在下次 do...while 循环中解决父节点(completedWork.return)
completedWork = returnFiber;
// 此处须要留神!// 尽管把 workInProgress 置为 completedWork,但因为没有 return,即没有完结 completeUnitOfWork,因而没有意义
// 直到 completedWork(此时实际上是本循环中原 completedWork.return)为 null,完结 do...while 循环后
// 此时 completeUnitOfWork 的运行后果 (workInProgress) 为 null
// 也意味着 performSyncWorkOnRoot/performConcurrentWorkOnRoot 中的 while 循环也达到了完结条件
workInProgress = completedWork;
} while (completedWork !== null);
// 省略...
}
请看流程图:
由流程图可知,completeUnitOfWork 次要做了两件事:执行 completeWork 和 收拢 EffectList,上面具体介绍一下这两块内容。
completeWork
如果说“递”阶段的 beginWork 办法次要是创立子节点,那么“归”阶段的 completeWork 办法则次要是创立以后节点的 DOM 节点,并对子节点的 DOM 节点和 EffectList 进行收拢。
相似 beginWork , completeWork 也会依据以后节点不同的 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;
}
// ... 省略
}
须要留神的是,很多类型的节点是没有 completeWork 这一块的逻辑的(即啥操作都没做就间接 return null
),比方十分常见的 Fragment 和 FunctionComponent。咱们重点关注页面渲染所必须的 HostComponent,即由 HTML 标签(如 <div></div>
)转换成的 Fiber 节点。
解决 HostComponent
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
const newProps = workInProgress.pendingProps;
switch (workInProgress.tag) {
// ... 省略
case HostComponent: {
// ... 省略
const type = workInProgress.type;
if (current !== null && workInProgress.stateNode != null) {
updateHostComponent(
current,
workInProgress,
type,
newProps,
rootContainerInstance,
);
// ... 省略
} else {
// ... 省略
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
appendAllChildren(instance, workInProgress, false, false);
workInProgress.stateNode = instance;
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {markUpdate(workInProgress);
}
}
return null;
}
// ... 省略
}
}
从下面这个代码段咱们能够得悉,completeWork 办法对 HostComponent 的解决次要有两个代码分支:
(current !== null && workInProgress.stateNode != null) === true
时,对以后节点做“更新”操作;(current !== null && workInProgress.stateNode != null) === true
时,对以后节点做“新建”操作;
这里之所以没有用之前文章里罕用的 mount(首屏渲染) 和 update 来表白,是因为存在一种状况,是 current !== null
而 workInProgress.stateNode === null
的:在 update 时,如果以后的 Fiber 节点是个新的节点,曾经在 beginWork 阶段被打上了 Placement effectTag,那么就会存在 stateNode 为 null 的状况;而在这种状况下,同样须要做“新建”操作。
HostComponent 的“更新”操作
在此代码分支中,因为曾经判断 workInProgress.stateNode !== null
,即已存在对应的 DOM 节点,所以不须要再生成 DOM 节点。
咱们能够看到这块次要是执行了一个 updateHostComponent 办法:
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
/* 如果 props 没有变动(以后节点是通过 bailoutOnAlreadyFinishedWork 办法来复用的),能够跳过对以后节点的解决 */
const oldProps = current.memoizedProps;
if (oldProps === newProps) {return;}
const instance: Instance = workInProgress.stateNode;
// 省略...
/* 计算须要变动的 DOM 节点属性,以数组形式存储(数组偶数索引的元素为属性名,数组基数索引的元素为属性值)*/
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// 将计算出来的 updatePayload 挂载在 workInProgress.updateQueue 上,供后续 commit 阶段应用
workInProgress.updateQueue = (updatePayload: any);
// 如果 updatePayload 不为空,则给以后节点打上 Update 的 EffectTag
if (updatePayload) {markUpdate(workInProgress);
}
};
从下面的代码段能够看出 updateHostComponent 的次要作用就是计算出须要变动的 DOM 节点属性,并给以后节点打上 Update 的 EffectTag。
prepareUpdate
接下来咱们看 prepareUpdate 办法是如何计算出须要变动的 DOM 节点属性的:
export function prepareUpdate(
domElement: Instance,
type: string,
oldProps: Props,
newProps: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): null | Array<mixed> {
// 省略 DEV 代码...
return diffProperties(
domElement,
type,
oldProps,
newProps,
rootContainerInstance,
);
}
能够看出 prepareUpdate 其实是间接调用了 diffProperties 办法。
diffProperties
diffProperties 办法的代码比拟多,我这边就不放源码了,大略讲一下过程:
- 对特定 tag(因为本场景是解决 HostComponent,因而 tag 即 html 标签名)的 lastProps & nextProps 做非凡解决,包含 input/select/textarea,举例:input 的 value 值可能会是个 number,而原生 input 的 value 只承受 string,因而这里须要转换数据类型。
-
遍历 lastProps:
- 如果该 prop 在 nextProps 中也存在,那么就跳过,相当于该 prop 没有变动,无需解决。
- 见到有 style 的 prop 就整顿到 styleUpdates 变量 (object) 中,这部分 style 属性被置为空值
- 把除以上状况外的 propKey 推动一个数组 (updatePayload) 中,另外再推一个 null 值进数组中,示意把该 prop 清空掉。
-
遍历 nextProps:
- 如果该 nextProp 与 lastProp 统一,即更新前后没有发生变化,则跳过。
- 见到有 style 的 prop 就整顿到 styleUpdates 变量中,留神这部分 style 属性是有值的
- 解决 DANGEROUSLY_SET_INNER_HTML
- 解决 children
- 除以上场景外,间接把 prop 的 key 和值都推动数组 (updatePayload) 中。
- 如果 styleUpdates 不为空,那么就把 ’style’ 和 styleUpdates 变量都推动数组 (updatePayload) 中。
- 返回 updatePayload。
updatePayload 是个数组,其中数组偶数索引的元素为 prop key
,数组基数索引的元素为 prop value
。
markUpdate
接着来看 markUpdate 办法,该办法其实很简略,就是在 workInProgress.effectTag
上打了个 Update EffectTag
。
function markUpdate(workInProgress: Fiber) {
// Tag the fiber with an update effect. This turns a Placement into
// a PlacementAndUpdate.
workInProgress.effectTag |= Update;
}
HostComponent 的“新建”操作
“新建”操作的次要逻辑包含三个:
- 为 Fiber 节点生成对应的 DOM 节点:createInstance 办法
- 将子孙 DOM 节点插入刚生成的 DOM 节点中:appendAllChildren 办法
- 初始化以后 DOM 节点的所有属性以及事件回调解决:finalizeInitialChildren 办法
createInstance
上面来看“为 Fiber 节点生成对应的 DOM 节点”的办法 —— createInstance:
export function createInstance(
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
internalInstanceHandle: Object,
): Instance {
let parentNamespace: string;
// 省略 DEV 代码段...
// 确定该 DOM 节点的命名空间(xmlns 属性),个别是 "http://www.w3.org/1999/xhtml"
parentNamespace = ((hostContext: any): HostContextProd);
// 创立 DOM 元素
const domElement: Instance = createElement(
type,
props,
rootContainerInstance,
parentNamespace,
);
// 在 DOM 对象上创立指向 fiber 节点对象的属性(指针),不便后续取用
precacheFiberNode(internalInstanceHandle, domElement);
// 在 DOM 对象上创立指向 props 的属性(指针),不便后续取用
updateFiberProps(domElement, props);
return domElement;
}
能够看出 createInstance 次要是调用了 createElement 办法来创立 DOM 元素;至于 createElement 本文不开展,有趣味能够看看 源码。
appendAllChildren
上面来看“将子孙 DOM 节点插入刚生成的 DOM 节点中”的办法 —— appendAllChildren :
// completeWork 是这样调用的:appendAllChildren(instance, workInProgress, false, false);
appendAllChildren = function(
parent: Instance, // 绝对于要 append 的子节点来说,completeWork 以后解决的节点就是父节点
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
let node = workInProgress.child; // 第一个子 Fiber 节点
/* 这个 while 循环实质上是一个深度优先遍历 */
while (node !== null) {if (node.tag === HostComponent || node.tag === HostText) {
// 如果是 html 标签或纯文本对应的子节点,则将以后子节点的 DOM 增加到父节点的 DOM 子节点列表开端
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) { // 先疏忽
appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {// ... 无操作} else if (node.child !== null) {
// 针对一些非凡类型的子节点,如 <Fragment />,尝试从子节点的子节点获取 DOM
node.child.return = node; // 设置好 return 指针,不便后续分别是否达到循环完结条件
node = node.child; // 循环主体由子节点变为子节点的子节点
continue; // 立刻发展新一轮的循环
}
if (node === workInProgress) {return; // 遍历“回归时”发现曾经达到遍历的完结条件,完结遍历}
// 若以后循环主体 node 已无兄弟节点(sibling),则进行“回归”;且如果“回归”一次后发现还是没有 sibling,将持续“回归”while (node.sibling === null) {if (node.return === null || node.return === workInProgress) {return; //“回归”过程中达到遍历的完结条件,完结遍历}
node = node.return; //“回归”的后果:将 node.return 作为下次循环的主体
}
// 走到这里就表明以后循环主体有 sibling
node.sibling.return = node.return; // 设置好 return 指针,不便后续分别是否达到循环完结条件
node = node.sibling; // 将 node.sibling 作为下次循环的主体
}
};
// appendInitialChild 实质上就是执行了 appendChild 这个原生的 DOM 节点办法
// https://developer.mozilla.org/zh-CN/docs/Web/API/Node/appendChild
export function appendInitialChild(parentInstance: Instance, child: Instance | TextInstance): void {parentInstance.appendChild(child);
}
appendAllChildren 实质上是一个有条件限度(限度递进档次)的深度优先遍历:
- 取出以后节点 (parent) 的第一个子节点作为循环主体(
node
)。 - 如果该循环主体是
html 标签或纯文本对应的 Fiber 节点
,则将其 DOMappendChild
给parent
。 - 如果以后循环主体 (
node
) 有兄弟节点(node.sibling
),则将该兄弟节点设为下次循环的主体。
光看下面这个流程,这不是一个典型的广度优先遍历吗?别急,因为还有一种比拟非凡的状况:当以后循环主体不是 html 标签或纯文本对应的 Fiber 节点
,且以后循环主体有子节点(node.child
) 时,将以后循环主体的子节点作为下次循环的主体,并立刻开始下次循环(continue
)。
以上面这个组件作为例子:
function App() {
return (
<div>
<b>1</b>
<Fragment>
<span>2</span>
<p>3</p>
</Fragment>
</div>
)
}
依据《React 源码解析系列 – React 的 render 阶段(一):根本流程介绍》里对 beginWork 和 completeWork 的执行程序能够得出:
1. rootFiber beginWork
2. App Fiber beginWork
3. div Fiber beginWork
4. b Fiber beginWork
5. b Fiber completeWork // 以后节点 —— <b />,appendChild 文本节点
6. Fragment Fiber beginWork
7. span Fiber beginWork
8. span Fiber completeWork // 以后节点 —— <span />,appendChild 文本节点
9. p Fiber beginWork
10. p Fiber completeWork // 以后节点 —— <p />,appendChild 文本节点
11. Fragment Fiber completeWork // 跳过
12. div Fiber completeWork // 上面咱们来重点介绍这一块
13. App Fiber completeWork
14. rootFiber completeWork
咱们来重点介绍 div 节点中的 appendAllChildren
:
- while 循环执行前初始化:取出 div 节点的第一个子节点 —— b 节点,作为第一次 while 循环的主体。
-
第一次 while 循环(循环主体为 b 节点):
- b 节点是一个 HostComponent,间接 appendChild。
- b 节点有一个兄弟节点,即 Fragment 节点,将其设置为下一次 while 循环的主体(
node
)。
-
第二次 while 循环(循环主体为 Fragment 节点):
- 因为 Fragment 节点既不是 HostComponent 也不是 HostText,因而将取 Fragment 节点的第一个子节点 —— span 节点作为下次 while 循环的主体(
node
)。 - 立刻进入 (
continue
) 下一次 while 循环。
- 因为 Fragment 节点既不是 HostComponent 也不是 HostText,因而将取 Fragment 节点的第一个子节点 —— span 节点作为下次 while 循环的主体(
-
第三次 while 循环(循环主体为 span 节点):
- span 节点是一个 HostComponent,间接 appendChild。
- span 节点有一个兄弟节点,即 p 节点,将其设置为下一次 while 循环的主体(
node
)。
-
第四次 while 循环(循环主体为 p 节点):
- p 节点是一个 HostComponent,间接 appendChild。
- p 节点没有兄弟节点,进行回归 (
node = node.return
),此时在该“回归”代码段 —— 一个小 while 循环
中,循环主体变为 p 节点的父节点,即 Fragment 节点。 - 持续下一次
小 while 循环
:因为 Fragment 也没有兄弟节点,不满足小 while 循环
的完结条件,因而持续进行“回归”,此时循环主体 (node
) 为 div 节点。 - 持续下一次
小 while 循环
:因为 div 节点满足node.return === workInProgress
,因而间接完结整个遍历过程 —— appendAllChildren。
finalizeInitialChildren
上面来看“初始化以后 DOM 节点的所有属性以及事件回调解决”—— finalizeInitialChildren:
export function finalizeInitialChildren(
domElement: Instance,
type: string,
props: Props,
rootContainerInstance: Container,
hostContext: HostContext,
): boolean {setInitialProperties(domElement, type, props, rootContainerInstance);
return shouldAutoFocusHostComponent(type, props);
}
从下面的代码段,咱们能够很清晰地看到 finalizeInitialChildren 次要分为两个步骤:
- 执行 setInitialProperties 办法;留神,该办法与 prepareUpdate 不一样,该办法是会真正将 DOM 属性挂载到 DOM 节点上的,也会真正地调用 addEventListener 把事件处理回调绑定在以后 DOM 节点上的。
- 执行 shouldAutoFocusHostComponent 办法:返回
props.autoFocus
的值(仅button
/input
/select
/textarea
反对)。
收拢 EffectList
作为 DOM 操作的根据,commit 阶段须要找到所有带有 effectTag 的 Fiber 节点并顺次执行 effectTag 对应操作,难道还须要在 commit 阶段再遍历一次 Fiber 树吗?这显然是很低效的。
为了解决这个问题,在 completeUnitOfWork 中,每个执行完 completeWork 且存在 effectTag 的 Fiber 节点会被保留在一条被称为 effectList 的单向链表中;effectList 中第一个 Fiber 节点保留在 fiber.firstEffect,最初一个元素保留在 fiber.lastEffect。
相似 appendAllChildren,在“归”阶段,所有有 effectTag 的 Fiber 节点都会被追加在父节点的 effectList 中,最终造成一条以 rootFiber.firstEffect 为终点的单向链表。
如果以后 Fiber 节点 (completedWork) 也有 EffectTag,那么将其放在 (EffectList 中) 子 Fiber 节点的前面。
/* 如果父节点的 effectList 头指针为空,那么就间接把本节点的 effectList 头指针赋给父节点的头指针,相当于把本节点的整个 effectList 间接挂在父节点中 */
if (returnFiber.firstEffect === null) {returnFiber.firstEffect = completedWork.firstEffect;}
/* 如果父节点的 effectList 不为空,那么就把本节点的 effectList 挂载在父节点 effectList 的前面 */
if (completedWork.lastEffect !== null) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork.firstEffect;}
returnFiber.lastEffect = completedWork.lastEffect;
}
/* 如果以后 Fiber 节点 (completedWork) 也有 EffectTag,那么将其放在 (EffectList 中) 子 Fiber 节点前面 */
const effectTag = completedWork.effectTag;
/* 跳过 NoWork/PerformedWork 这两种 EffectTag 的节点,NoWork 就不必解释了,PerformedWork 是给 DevTools 用的 */
if (effectTag > PerformedWork) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}
returnFiber.lastEffect = completedWork;
}
}
completeUnitOfWork 完结
completeUnitOfWork 有两种完结的场景:
- 以后节点 (
completed
) 有兄弟节点(completed.sibling
),此时会将 workInProgress(即 performUnitOfWork 的循环主体)设为该兄弟节点,而后完结掉 completeUnitOfWork 办法,尔后将进行下一次 performUnitOfWork,换句话说:执行该“兄弟节点”的“递”阶段 —— beginWork。 - 在 completeUnitOfWork“回归”的过程中,
completed
的值为null
,即以后已实现整棵 Fiber 树的回归;此时,workInProgress 的值为 null,这意味着 workLoopSync / workLoopConcurrent 办法中的 while 循环也达到了完结条件;至此,React 的 render 阶段完结。
当 render 阶段完结时,在 performSyncWorkOnRoot 办法中,会调用 commitRoot(root)
来开启 React commit 阶段的工作。