点击进入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树根本构建实现。在这个过程中咱们能够总结出几个法则:

  1. 向节点中插入dom节点时,只插入它子节点中第一层的dom。能够把这个插入能够看成是一个自下而上收集dom节点的过程。第一层子节点之下的dom,曾经在第一层子节点执行插入时被插入第一层子节点了,从下往上逐层completeWork

的这个过程相似于dom节点的累加。

  1. 总是优先看自身可否插入,再往下找,之后才是找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组件,一旦类组件中应用了getDerivedStateFromErrorcomponentDidCatch,就能够捕捉产生在其子树中的谬误,那么它就是谬误边界。

回到源码中,节点如果在更新的过程中报错,它就会被打上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的时候,会被解决。
这样保障了getDerivedStateFromErrorcomponentDidCatch的调用,而后产生新的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的子树中有某个节点产生了谬误,组件中的getDerivedStateFromErrorcomponentDidCatch就会被触发,
此时的备用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阶段的各个过程。

欢送扫码关注公众号,发现更多技术文章