点击进入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是,不满足node.tag === HostComponent || node.tag === HostText
的条件,所以不会将它插入到div中。继续向下找的child,发现是p,将P插入div,然后寻找p的sibling,发现了'text',将它也插入div。之后再也找不到同级节点,此时回到第三层的节点。
有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添加到updatePayloadlastProps中存在,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阶段的各个过程。