点击进入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并入到父节点的effectListif (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阶段的各个过程。
欢送扫码关注公众号,发现更多技术文章