点击进入 React 源码调试仓库。
概述
每个 fiber 节点在更新时都会经验两个阶段:beginWork 和 completeWork。在 Diff 之后(详见深刻了解 React Diff 原理),workInProgress 节点就会进入 complete 阶段。这个时候拿到的 workInProgress 节点都是通过 diff 算法和谐过的,也就意味着对于某个节点来说它 fiber 的状态曾经根本确定了,但除此之外还有两点:
- 目前只有 fiber 状态变了,对于原生 DOM 组件(HostComponent)和文本节点(HostText)的 fiber 来说,对应的 DOM 节点(fiber.stateNode)并未变动。
- 通过 Diff 生成的新的 workInProgress 节点持有了 flag(即 effectTag)
基于这两个特点,completeWork 的工作次要有:
-
构建或更新 DOM 节点,
- 构建过程中,会自下而上将子节点的第一层第一层插入到以后节点。
- 更新过程中,会计算 DOM 节点的属性,一旦属性须要更新,会为 DOM 节点对应的 workInProgress 节点标记 Update 的 effectTag。
- 自下而上收集 effectList,最终收集到 root 上
对于失常执行工作的 workInProgress 节点来说,会走以上的流程。然而免不了节点的更新会出错,所以对出错的节点会采取措施,这波及到谬误边界以及 Suspense 的概念,
本文只做简略流程剖析。
这一节波及的知识点有
- DOM 节点的创立以及挂载
- DOM 属性的解决
- effectList 的收集
- 错误处理
流程
completeUnitOfWork 是 completeWork 阶段的入口。它外部有一个循环,会自下而上地遍历 workInProgress 节点,顺次解决节点。
对于失常的 workInProgress 节点,会执行 completeWork。这其中会对 HostComponent 组件实现更新 props、绑定事件等 DOM 相干的工作。
function completeUnitOfWork(unitOfWork: Fiber): void {
let completedWork = unitOfWork;
do {
const current = completedWork.alternate;
const returnFiber = completedWork.return;
if ((completedWork.effectTag & Incomplete) === NoEffect) {
// 如果 workInProgress 节点没有出错,走失常的 complete 流程
...
let next;
// 省略了判断逻辑
// 对节点进行 completeWork,生成 DOM,更新 props,绑定事件
next = completeWork(current, completedWork, subtreeRenderLanes);
if (next !== null) {
// 工作被挂起的状况,workInProgress = next;
return;
}
// 收集 workInProgress 节点的 lanes,不漏掉被跳过的 update 的 lanes,便于再次发动调度
resetChildLanes(completedWork);
// 将以后节点的 effectList 并入父级节点
...
// 如果以后节点他本人也有 effectTag,将它本人
// 也并入到父级节点的 effectList
} else {
// 执行到这个分支阐明之前的更新有谬误
// 进入 unwindWork
const next = unwindWork(completedWork, subtreeRenderLanes);
...
}
// 查找兄弟节点,若有则进行 beginWork -> completeWork
const siblingFiber = completedWork.sibling;
if (siblingFiber !== null) {
workInProgress = siblingFiber;
return;
}
// 若没有兄弟节点,那么向上回到父级节点
// 父节点进入 complete
completedWork = returnFiber;
// 将 workInProgress 节点指向父级节点
workInProgress = completedWork;
} while (completedWork !== null);
// 达到了 root,整棵树实现了工作,标记实现状态
if (workInProgressRootExitStatus === RootIncomplete) {workInProgressRootExitStatus = RootCompleted;}
}
因为 React 的大部分的 fiber 节点最终都要体现为 DOM,所以本文次要剖析 HostComponent 相干的解决流程。
function completeWork(
current: Fiber | null,
workInProgress: Fiber,
renderLanes: Lanes,
): Fiber | null {
...
switch (workInProgress.tag) {
...
case HostComponent: {
...
if (current !== null && workInProgress.stateNode != null) {// 更新} else {// 创立}
return null;
}
case HostText: {
const newText = newProps;
if (current && workInProgress.stateNode != null) {// 更新} else {// 创立}
return null;
}
case SuspenseComponent:
...
}
}
由 completeWork 的构造能够看出,就是根据 fiber 的 tag 做不同解决。对 HostComponent 和 HostText 的解决是相似的,都是针对它们的 DOM 节点,解决办法又会分为更新和创立。
若 current 存在并且 workInProgress.stateNode(workInProgress 节点对应的 DOM 实例)存在,阐明此 workInProgress 节点的 DOM 节点曾经存在,走更新逻辑,否则进行创立。
DOM 节点的更新实则是属性的更新,会在上面的 DOM 属性的解决 -> 属性的更新
中讲到,先来看一下 DOM 节点的创立和插入。
DOM 节点的创立和插入
咱们晓得,此时的 completeWork 解决的是通过 diff 算法之后产生的新 fiber。对于 HostComponent 类型的新 fiber 来说,它可能有 DOM 节点,也可能没有。没有的话,
就须要执行先创立,再插入的操作,由此引入 DOM 的插入算法。
if (current !== null && workInProgress.stateNode != null) {// 表明 fiber 有 dom 节点,须要执行更新过程} else {
// fiber 不存在 DOM 节点
// 先创立 DOM 节点
const instance = createInstance(
type,
newProps,
rootContainerInstance,
currentHostContext,
workInProgress,
);
//DOM 节点插入
appendAllChildren(instance, workInProgress, false, false);
// 将 DOM 节点挂载到 fiber 的 stateNode 上
workInProgress.stateNode = instance;
...
}
须要留神的是,DOM 的插入并不是将以后 DOM 插入它的父节点,而是将以后这个 DOM 节点的第一层子节点插入到它本人的上面。
图解算法
此时的 completeWork 阶段,会自下而上遍历 workInProgress 树到 root,每通过一层都会依照下面的规定插入 DOM。下边用一个例子来了解一下这个过程。
这是一棵 fiber 树的构造,workInProgress 树最终要成为这个状态。
1 App
|
|
2 div
/
/
3 <List/>--->span
/
/
4 p ----> 'text node'
/
/
5 h1
构建 workInProgress 树的 DFS 遍历对沿途节点一路 beginWork,此时曾经遍历到最深的 h1 节点,它的 beginWork 曾经完结,开始进入 completeWork 阶段,此时所在的层级深度为第 5 层。
第 5 层
1 App
|
|
2 div
/
/
3 <List/>
/
/
4 p
/
/
5--->h1
此时 workInProgress 节点指向 h1 的 fiber,它对应的 dom 节点为 h1,dom 标签创立进去当前进入appendAllChildren
,因为以后的 workInProgress 节点为 h1,所以它的 child 为 null,无子节点可插入,退出。
h1 节点实现工作往上返回到第 4 层的 p 节点。
此时的 dom 树为
h1
第 4 层
1 App
|
|
2 div
/
/
3 <List/>
/
/
4 ---> p ----> 'text node'
/
/
5 h1
此时 workInProgress 节点指向 p 的 fiber,它对应的 dom 节点为 p,进入 appendAllChildren
,发现 p 的 child 为 h1,并且是 HostComponent 组件,将 h1 插入 p,而后寻找子节点 h1 是否有同级的 sibling 节点。
发现没有,退出。
p 节点的所有工作实现,它的兄弟节点:HostText 类型的组件 ’text’ 会作为下一个工作单元,执行 beginWork 再进入 completeWork。当初须要对它执行 appendAllChildren
,发现没有 child,
不执行插入操作。它的工作也实现,return 到父节点<List/>
,进入第 3 层
此时的 dom 树为
p 'text'
/
/
h1
第 3 层
1 App
|
|
2 div
/
/
3 ---> <List/>--->span
/
/
4 p ----> 'text'
/
/
5 h1
此时 workInProgress 节点指向 <List/>
的 fiber,对它进行 completeWork,因为此时它是自定义组件,不属于 HostComponent,所以不会对它进行子节点的插入操作。
寻找它的兄弟节点 span,对 span 先进行 beginWork 再进行到 completeWork,执行 span 子节点的插入操作,发现它没有 child,退出。return 到父节点 div,进入第二层。
此时的 dom 树为
span
p 'text'
/
/
h1
第 2 层
1 App
|
|
2 ---------> div
/
/
3 <List/>--->span
/
/
4 p ---->'text'
/
/
5 h1
此时 workInProgress 节点指向 div 的 fiber,对它进行 completeWork,执行 div 的子节点插入。因为它的 child 是 <List/>,不满足 node.tag === HostComponent || node.tag === HostText
的条件,所以
不会将它插入到 div 中。持续向下找 <List/> 的 child,发现是 p,将 P 插入 div,而后寻找 p 的 sibling,发现了 ’text’,将它也插入 div。之后再也找不到同级节点,此时回到第三层的 <List/> 节点。
<List/> 有 sibling 节点 span,将 span 插入到 div。因为 span 没有子节点,退出。
此时的 dom 树为
div
/ | \
/ | \
p 'text' span
/
/
h1
第 1 层
此时 workInProgress 节点指向 App 的 fiber,因为它是自定义节点,所以不会对它进行子节点的插入操作。
到此为止,dom 树根本构建实现。在这个过程中咱们能够总结出几个法则:
- 向节点中插入 dom 节点时,只插入它子节点中第一层的 dom。能够把这个插入能够看成是一个自下而上收集 dom 节点的过程。第一层子节点之下的 dom,曾经在第一层子节点执行插入时被插入第一层子节点了,从下往上逐层 completeWork
的这个过程相似于 dom 节点的累加。
- 总是优先看自身可否插入,再往下找,之后才是找 sibling 节点。
这是因为 fiber 树和 dom 树的差别导致,每个 fiber 节点不肯定对应一个 dom 节点,但一个 dom 节点肯定对应一个 fiber 节点。
fiber 树 DOM 树
<App/> div
| |
div input
|
<Input/>
|
input
因为一个原生 DOM 组件的子组件有可能是类组件或函数组件,所以会优先查看本身,发现自己不是原生 DOM 组件,不能被插入到父级 fiber 节点对应的 DOM 中,所以要往下找,直到找到原生 DOM 组件,执行插入,
最初再从这一层找同级的 fiber 节点,同级节点也会执行 先自检,再查看上级,再查看上级的同级
的操作。
能够看出,节点的插入也是深度优先。值得注意的是,这一整个插入的流程并没有真的将 DOM 插入到实在的页面上,它只是在操作 fiber 上的 stateNode。实在的插入 DOM 操作产生在 commit 阶段。
节点插入源码
上面是插入节点算法的源码,能够对照下面的过程来看。
appendAllChildren = function(
parent: Instance,
workInProgress: Fiber,
needsVisibilityToggle: boolean,
isHidden: boolean,
) {
// 找到以后节点的子 fiber 节点
let node = workInProgress.child;
// 当存在子节点时,去往下遍历
while (node !== null) {if (node.tag === HostComponent || node.tag === HostText) {
// 子节点是原生 DOM 节点,间接能够插入
appendInitialChild(parent, node.stateNode);
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {appendInitialChild(parent, node.stateNode.instance);
} else if (node.tag === HostPortal) {// 如果是 HostPortal 类型的节点,什么都不做} else if (node.child !== null) {
// 代码执行到这,阐明 node 不合乎插入要求,// 持续寻找子节点
node.child.return = node;
node = node.child;
continue;
}
if (node === workInProgress) {return;}
// 当不存在兄弟节点时往上找,此过程产生在以后 completeWork 节点的子节点再无子节点的场景,// 并不是间接从以后 completeWork 的节点去往上找
while (node.sibling === null) {if (node.return === null || node.return === workInProgress) {return;}
node = node.return;
}
// 当不存在子节点时,从 sibling 节点动手开始找
node.sibling.return = node.return;
node = node.sibling;
}
};
DOM 属性的解决
下面的插入过程实现了 DOM 树的构建,这之后要做的就是为每个 DOM 节点计算它本人的属性(props)。因为节点存在创立和更新两种状况,所以对属性的解决也会区别对待。
属性的创立
属性的创立绝对更新来说比较简单,这个过程产生在 DOM 节点构建的最初,调用 finalizeInitialChildren
函数实现新节点的属性设置。
if (current !== null && workInProgress.stateNode != null) {// 更新} else {
...
// 创立、插入 DOM 节点的过程
...
// DOM 节点属性的初始化
if (
finalizeInitialChildren(
instance,
type,
newProps,
rootContainerInstance,
currentHostContext,
)
) {
// 最终会根据 textarea 的 autoFocus 属性
// 来决定是否更新 fiber
markUpdate(workInProgress);
}
}
finalizeInitialChildren
最终会调用 setInitialProperties
,来实现属性的设置。
过程好了解,次要就是调用 setInitialDOMProperties
将属性间接设置进 DOM 节点(事件在这个阶段绑定)
function setInitialDOMProperties(
tag: string,
domElement: Element,
rootContainerElement: Element | Document,
nextProps: Object,
isCustomComponentTag: boolean,
): void {for (const propKey in nextProps) {const nextProp = nextProps[propKey];
if (propKey === STYLE) {
// 设置行内款式
setValueForStyles(domElement, nextProp);
} else if (propKey === DANGEROUSLY_SET_INNER_HTML) {
// 设置 innerHTML
const nextHtml = nextProp ? nextProp[HTML] : undefined;
if (nextHtml != null) {setInnerHTML(domElement, nextHtml);
}
}
...
else if (registrationNameDependencies.hasOwnProperty(propKey)) {
// 绑定事件
if (nextProp != null) {ensureListeningTo(rootContainerElement, propKey);
}
} else if (nextProp != null) {
// 设置其余属性
setValueForProperty(domElement, propKey, nextProp, isCustomComponentTag);
}
}
}
属性的更新
若对已有 DOM 节点进行更新,阐明只对属性进行更新即可,因为节点曾经存在,不存在删除和新增的状况。updateHostComponent
函数
负责 HostComponent 对应 DOM 节点属性的更新,代码不多很好了解。
updateHostComponent = function(
current: Fiber,
workInProgress: Fiber,
type: Type,
newProps: Props,
rootContainerInstance: Container,
) {
const oldProps = current.memoizedProps;
// 新旧 props 雷同,不更新
if (oldProps === newProps) {return;}
const instance: Instance = workInProgress.stateNode;
const currentHostContext = getHostContext();
// prepareUpdate 计算新属性
const updatePayload = prepareUpdate(
instance,
type,
oldProps,
newProps,
rootContainerInstance,
currentHostContext,
);
// 最终新属性被挂载到 updateQueue 中,供 commit 阶段应用
workInProgress.updateQueue = (updatePayload: any);
if (updatePayload) {
// 标记 workInProgress 节点有更新
markUpdate(workInProgress);
}
};
能够看出它只做了一件事,就是计算新的属性,并挂载到 workInProgress 节点的 updateQueue 中,它的模式是以 2 为单位,index 为偶数的是 key,为奇数的是 value:
['style', { color: 'blue'}, title, '测试题目' ]
这个后果由 diffProperties
计算产生,它比照 lastProps 和 nextProps,计算出 updatePayload。
举个例子来说,有如下组件,div 上绑定的点击事件会扭转它的 props。
class PropsDiff extends React.Component {
state = {
title: '更新前的题目',
color: 'red',
fontSize: 18
}
onClickDiv = () => {
this.setState({
title: '更新后的题目',
color: 'blue'
})
}
render() {const { color, fontSize, title} = this.state
return <div
className="test"
onClick={this.onClickDiv}
title={title}
style={{color, fontSize}}
{...this.state.color === 'red' && { props: '自定义旧属性'}}
>
测试 div 的 Props 变动
</div>
}
}
lastProps 和 nextProps 别离为
lastProps
{
"className": "test",
"title": "更新前的题目",
"style": {"color": "red", "fontSize": 18},
"props": "自定义旧属性",
"children": "测试 div 的 Props 变动",
"onClick": () => {...}
}
nextProps
{
"className": "test",
"title": "更新后的题目",
"style": {"color":"blue", "fontSize":18},
"children": "测试 div 的 Props 变动",
"onClick": () => {...}
}
它们有变动的是 propsKey 是style、title、props
,通过 diff,最终打印进去的 updatePayload 为
[
"props", null,
"title", "更新后的题目",
"style", {"color":"blue"}
]
diffProperties
外部的规定能够概括为:
若有某个属性(propKey),它在
- lastProps 中存在,nextProps 中不存在,将 propKey 的 value 标记为 null 示意删除
- lastProps 中不存在,nextProps 中存在,将 nextProps 中的 propKey 和对应的 value 增加到 updatePayload
- lastProps 中存在,nextProps 中也存在,将 nextProps 中的 propKey 和对应的 value 增加到 updatePayload
对照这个规定看一下源码:
export function diffProperties(
domElement: Element,
tag: string,
lastRawProps: Object,
nextRawProps: Object,
rootContainerElement: Element | Document,
): null | Array<mixed> {
let updatePayload: null | Array<any> = null;
let lastProps: Object;
let nextProps: Object;
...
let propKey;
let styleName;
let styleUpdates = null;
for (propKey in lastProps) {
// 循环 lastProps,找出须要标记删除的 propKey
if (nextProps.hasOwnProperty(propKey) ||
!lastProps.hasOwnProperty(propKey) ||
lastProps[propKey] == null
) {
// 对 propKey 来说,如果 nextProps 也有,或者 lastProps 没有,那么
// 就不须要标记为删除,跳出本次循环持续判断下一个 propKey
continue;
}
if (propKey === STYLE) {
// 删除 style
const lastStyle = lastProps[propKey];
for (styleName in lastStyle) {if (lastStyle.hasOwnProperty(styleName)) {if (!styleUpdates) {styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
} else if(/*...*/) {
...
// 一些特定品种的 propKey 的删除
} else {
// 将其余品种的 propKey 标记为删除
(updatePayload = updatePayload || []).push(propKey, null);
}
}
for (propKey in nextProps) {
// 将新 prop 增加到 updatePayload
const nextProp = nextProps[propKey];
const lastProp = lastProps != null ? lastProps[propKey] : undefined;
if (!nextProps.hasOwnProperty(propKey) ||
nextProp === lastProp ||
(nextProp == null && lastProp == null)
) {
// 如果 nextProps 不存在 propKey,或者前后的 value 雷同,或者前后的 value 都为 null
// 那么不须要增加进去,跳出本次循环持续解决下一个 prop
continue;
}
if (propKey === STYLE) {
/*
* lastProp: {color: 'red'}
* nextProp: {color: 'blue'}
* */
// 如果 style 在 lastProps 和 nextProps 中都有
// 那么须要删除 lastProps 中 style 的款式
if (lastProp) {
// 如果 lastProps 中也有 style
// 将 style 内的款式属性设置为空
// styleUpdates = {color: ''}
for (styleName in lastProp) {
if (lastProp.hasOwnProperty(styleName) &&
(!nextProp || !nextProp.hasOwnProperty(styleName))
) {if (!styleUpdates) {styleUpdates = {};
}
styleUpdates[styleName] = '';
}
}
// 以 nextProp 的属性名为 key 设置新的 style 的 value
// styleUpdates = {color: 'blue'}
for (styleName in nextProp) {
if (nextProp.hasOwnProperty(styleName) &&
lastProp[styleName] !== nextProp[styleName]
) {if (!styleUpdates) {styleUpdates = {};
}
styleUpdates[styleName] = nextProp[styleName];
}
}
} else {
// 如果 lastProps 中没有 style,阐明新增的
// 属性全副可放入 updatePayload
if (!styleUpdates) {if (!updatePayload) {updatePayload = [];
}
updatePayload.push(propKey, styleUpdates);
// updatePayload: [style, null]
}
styleUpdates = nextProp;
// styleUpdates = {color: 'blue'}
}
} else if (/*...*/) {
...
// 一些特定品种的 propKey 的解决
} else if (registrationNameDependencies.hasOwnProperty(propKey)) {if (nextProp != null) {
// 从新绑定事件
ensureListeningTo(rootContainerElement, propKey);
}
if (!updatePayload && lastProp !== nextProp) {
// 事件从新绑定后,须要赋值 updatePayload,使这个节点得以被更新
updatePayload = [];}
} else if (
typeof nextProp === 'object' &&
nextProp !== null &&
nextProp.$$typeof === REACT_OPAQUE_ID_TYPE
) {
// 服务端渲染相干
nextProp.toString();} else {
// 将计算好的属性 push 到 updatePayload
(updatePayload = updatePayload || []).push(propKey, nextProp);
}
}
if (styleUpdates) {
// 将 style 和值 push 进 updatePayload
(updatePayload = updatePayload || []).push(STYLE, styleUpdates);
}
console.log('updatePayload', JSON.stringify(updatePayload));
// ['style', { color: 'blue'}, title, '测试题目' ]
return updatePayload;
}
DOM 节点属性的 diff 为 workInProgress 节点挂载了带有新属性的 updateQueue,一旦节点的 updateQueue 不为空,它就会被标记上 Update 的
effectTag,commit 阶段会解决 updateQueue。
if (updatePayload) {markUpdate(workInProgress);
}
effect 链的收集
通过 beginWork 和下面对于 DOM 的操作,有变动的 workInProgress 节点曾经被打上了 effectTag。
一旦 workInProgress 节点持有了 effectTag,阐明它须要在 commit 阶段被解决。每个 workInProgress 节点都有一个 firstEffect 和 lastEffect,是一个单向链表,来表
示它本身以及它的子节点上所有持有 effectTag 的 workInProgress 节点。completeWork 阶段在向上遍历的过程中也会逐层收集 effect 链,最终收集到 root 上,
供接下来的 commit 阶段应用。
实现上绝对简略,对于某个 workInProgress 节点来说,先将它已有的 effectList 并入到父级节点,再判断它本人有没有 effectTag,有的话也并入到父级节点。
/*
* effectList 是一条单向链表,每实现一个工作单元上的工作,* 都要将它产生的 effect 链表并入
* 下级工作单元。* */
// 将以后节点的 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;
}
// 将本身增加到 effect 链,增加时跳过 NoWork 和
// PerformedWork 的 effectTag,因为真正
// 的 commit 用不到
const effectTag = completedWork.effectTag;
if (effectTag > PerformedWork) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}
returnFiber.lastEffect = completedWork;
}
每个节点都会执行这样的操作,最终当回到 root 的时候,root 上会有一条残缺的 effectList,蕴含了所有须要解决的 fiber 节点。
错误处理
completeUnitWork 中的错误处理是谬误边界机制的组成部分。
谬误边界是一种 React 组件,一旦类组件中应用了 getDerivedStateFromError
或componentDidCatch
,就能够捕捉产生在其子树中的谬误,那么它就是谬误边界。
回到源码中,节点如果在更新的过程中报错,它就会被打上 Incomplete 的 effectTag,阐明节点的更新工作未实现,因而不能执行失常的 completeWork,
要走另一个判断分支进行解决。
if ((completedWork.effectTag & Incomplete) === NoEffect) {
} else {// 有 Incomplete 的节点会进入到这个判断分支进行错误处理}
Incomplete 从何而来
什么状况下节点会被标记上 Incomplete 呢?这还要从最外层的工作循环说起。
concurrent 模式的渲染函数:renderRootConcurrent 之中在构建 workInProgress 树时,应用了 try…catch 来包裹执行函数,这对解决报错节点提供了机会。
do {
try {workLoopConcurrent();
break;
} catch (thrownValue) {handleError(root, thrownValue);
}
} while (true);
一旦某个节点执行出错,会进入 handleError
函数解决。该函数中能够获取到以后出错的 workInProgress 节点,除此之外咱们暂且不关注其余性能,只需分明它调用了throwException
。
throwException
会为这个出错的 workInProgress 节点打上 Incomplete 的 effectTag
,表明未实现,在向上找到能够处理错误的节点(即谬误边界),增加上 ShouldCapture 的 effectTag。
另外,创立代表谬误的 update,getDerivedStateFromError
放入 payload,componentDidCatch
放入 callback。最初这个 update 入队节点的 updateQueue。
throwException
执行结束,回到出错的 workInProgress 节点,执行completeUnitOfWork
,目标是将谬误终止到以后的节点,因为它自身都出错了,再向下渲染没有意义。
function handleError(root, thrownValue):void {
...
// 给以后出错的 workInProgress 节点增加上 Incomplete 的 effectTag
throwException(
root,
erroredWork.return,
erroredWork,
thrownValue,
workInProgressRootRenderLanes,
);
// 开始对谬误节点执行 completeWork 阶段
completeUnitOfWork(erroredWork);
...
}
重点:从产生谬误的节点往上找到谬误边界,做记号,记号就是 ShouldCapture 的 effectTag。
谬误边界再次更新
当这个谬误节点进入 completeUnitOfWork 时,因为持有了Incomplete
,所以不会进入失常的 complete 流程,而是会进入错误处理的逻辑。
错误处理逻辑做的事件:
- 对出错节点执行
unwindWork
。 - 将出错节点的父节点(returnFiber)标记上
Incomplete
,目标是在父节点执行到 completeUnitOfWork 的时候,也能被执行 unwindWork,进而验证它是否是谬误边界。 - 清空出错节点父节点上的 effect 链。
这里的重点是 unwindWork
会验证节点是否是谬误边界,来看一下 unwindWork 的要害代码:
function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {switch (workInProgress.tag) {
case ClassComponent: {
...
const effectTag = workInProgress.effectTag;
if (effectTag & ShouldCapture) {
// 删它下面的 ShouldCapture,再打上 DidCapture
workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
return workInProgress;
}
return null;
}
...
default:
return null;
}
}
unwindWork
验证节点是谬误边界的根据就是节点上是否有刚刚 throwException
的时候打上的 ShouldCapture 的 effectTag。如果验证胜利,最终会被 return 进来。
return 进来之后呢?会被赋值给 workInProgress 节点,咱们往下看一下错误处理的整体逻辑:
if ((completedWork.effectTag & Incomplete) === NoEffect) {
// 失常流程
...
} else {
// 验证节点是否是谬误边界
const next = unwindWork(completedWork, subtreeRenderLanes);
if (next !== null) {
// 如果找到了谬误边界,删除与错误处理无关的 effectTag,// 例如 ShouldCapture、Incomplete,// 并将 workInProgress 指针指向 next
next.effectTag &= HostEffectMask;
workInProgress = next;
return;
}
// ... 省略了 React 性能剖析相干的代码
if (returnFiber !== null) {
// 将父 Fiber 的 effect list 革除,effectTag 标记为 Incomplete,// 便于它的父节点再 completeWork 的时候被 unwindWork
returnFiber.firstEffect = returnFiber.lastEffect = null;
returnFiber.effectTag |= Incomplete;
}
}
...
// 持续向上 completeWork 的过程
completedWork = returnFiber;
当初咱们要有个认知,一旦 unwindWork 辨认以后的 workInProgress 节点为谬误边界,那么当初的 workInProgress 节点就是这个谬误边界。
而后会删除掉与错误处理无关的 effectTag,DidCapture 会被保留下来。
if (next !== null) {
next.effectTag &= HostEffectMask;
workInProgress = next;
return;
}
重点:将 workInProgress 节点指向谬误边界,这样能够对谬误边界从新走更新流程。
这个时候 workInProgress 节点有值,并且跳出了 completeUnitOfWork,那么持续最外层的工作循环:
function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);
}
}
此时,workInProgress 节点,也就是谬误边界,它会 再被 performUnitOfWork 解决,而后进入 beginWork、completeWork!
也就是说它会被从新更新一次。为什么说再被更新呢?因为构建 workInProgress 树的时候,beginWork 是从上往下的,过后 workInProgress 指针指向它的时候,它只执行了 beginWork。
此时子节点出错导致向上 completeUnitOfWork 的时候,发现了他是谬误边界,workInProgress 又指向了它,所以它会再次进行 beginWork。不同的是,这次节点上持有了
DidCapture 的 effectTag。所以流程上是不一样的。
还记得 throwException
阶段入队谬误边界更新队列的示意谬误的 update 吗?它在此次 beginWork 调用 processUpdateQueue 的时候,会被解决。
这样保障了 getDerivedStateFromError
和componentDidCatch
的调用,而后产生新的 state,这个 state 示意这次谬误的状态。
谬误边界是类组件,在 beginWork 阶段会执行 finishClassComponent
,如果判断组件有 DidCapture,会卸载掉它所有的子节点,而后从新渲染新的子节点,
这些子节点有可能是通过错误处理渲染的备用 UI。
示例代码来自 React 谬误边界介绍
class ErrorBoundary extends React.Component {constructor(props) {super(props);
this.state = {hasError: false};
}
static getDerivedStateFromError(error) {
// 更新 state 使下一次渲染可能显示降级后的 UI
return {hasError: true};
}
componentDidCatch(error, errorInfo) {
// 你同样能够将谬误日志上报给服务器
logErrorToMyService(error, errorInfo);
}
render() {if (this.state.hasError) {
// 你能够自定义降级后的 UI 并渲染
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
对于上述情况来说,一旦 ErrorBoundary 的子树中有某个节点产生了谬误,组件中的 getDerivedStateFromError
和 componentDidCatch
就会被触发,
此时的备用 UI 就是:
<h1>Something went wrong.</h1>
流程梳理
下面的错误处理咱们用图来梳理一下,假如 <Example/>
具备错误处理的能力。
1 App
|
|
2 <Example/>
/
/
3 ---> <List/>--->span
/
/
4 p ----> 'text'
/
/
5 h1
1. 如果 <List/>
更新出错,那么首先 throwException
会给它打上 Incomplete 的 effectTag,而后以它的父节点为终点向上找到能够处理错误的节点。
2. 找到了 <Example/>
,它能够处理错误,给他打上 ShouldCapture 的 effectTag(做记号),创立谬误的 update,将getDerivedStateFromError
放入 payload,componentDidCatch
放入 callback。
,入队 <Example/>
的 updateQueue。
3. 从 <List/>
开始间接 completeUnitOfWork
。因为它有 Incomplete,所以会走unwindWork
,而后给它的父节点<Example/>
打上 Incomplete,unwindWork
发现它不是刚刚做记号的谬误边界,
持续向上completeUnitOfWork
。
4.<Example/>
有 Incomplete,进入unwindWork
,而它恰好是刚刚做过记号的谬误边界节点,去掉 ShouldCapture 打上 DidCapture,将 workInProgress 的指针指向<Example/>
5.<Example/>
从新进入 beginWork 解决 updateQueue,和谐子节点(卸载掉原有的子节点,渲染备用 UI)。
咱们能够看进去,React 的谬误边界的概念其实是对能够处理错误的组件从新进行更新。谬误边界只能捕捉它子树的谬误,而不能捕捉到它本人的谬误,本人的谬误要靠它下面的谬误边界来捕捉。
我想这是因为出错的组件曾经无奈再渲染出它的子树,也就意味着它不能渲染出备用 UI,所以即便它捕捉到了本人的谬误也于事无补。
这一点在 throwException
函数中有体现,是从它的父节点开始向上找谬误边界:
// 从以后节点的父节点开始向上找
let workInProgress = returnFiber;
do {...} while (workInProgress !== null);
回到 completeWork,它在整体的错误处理中做的事件就是对谬误边界内的节点进行解决:
- 查看以后节点是否是谬误边界,是的话将 workInProgress 指针指向它,便于它再次走一遍更新。
- 置空节点上的 effectList。
以上咱们只是剖析了个别场景下的错误处理,实际上在工作挂起(Suspense)时,也会走错误处理的逻辑,因为此时 throw 的谬误值是个 thenable 对象,具体会在剖析 suspense 时具体解释。
总结
workInProgress 节点的 completeWork 阶段次要做的事件再来回顾一下:
- 实在 DOM 节点的创立以及挂载
- DOM 属性的解决
- effectList 的收集
- 错误处理
尽管用了不少的篇幅去讲错误处理,然而依然须要重点关注失常节点的处理过程。completeWork 阶段处在 beginWork 之后,commit 之前,起到的是一个承前启后的作用。它接管到的是通过 diff 后的 fiber 节点,而后他本人要将 DOM 节点和 effectList 都筹备好。因为 commit 阶段是不能被打断的,所以充分准备有利于 commit 阶段做更少的工作。
一旦 workInProgress 树的所有节点都实现 complete,则阐明 workInProgress 树曾经构建实现,所有的更新工作曾经做完,接下来这棵树会进入 commit 阶段,从下一篇文章开始,咱们会剖析 commit 阶段的各个过程。
欢送扫码关注公众号,发现更多技术文章