乐趣区

关于SegmentFault:深挖React的completeWork

点击进入 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 并入到父节点的 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 组件,一旦类组件中应用了 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 阶段的各个过程。

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

退出移动版