关于react.js:React-源码解析系列-React-的-render-异常处理机制

8次阅读

共计 24478 个字符,预计需要花费 62 分钟才能阅读完成。

系列文章目录(同步更新)

  • React 源码解析系列 – React 的 render 阶段(一):根本流程介绍
  • React 源码解析系列 – React 的 render 阶段(二):beginWork
  • React 源码解析系列 – React 的 render 阶段(三):completeUnitOfWork
  • React 源码解析系列 – React 的 render 异样解决机制

本系列文章均为探讨 React v17.0.0-alpha 的源码

谬误边界(Error Boundaries)

在解释 React 外部实现前,我想先从一个 React API —— 谬误边界(Error Boundaries) 这一 React 异样解决机制 的“冰山一角”开始介绍。

谬误边界是什么

在 React 16 之前,React 并没有对开发者提供 API 来解决组件渲染过程中抛出的异样:

  • 这里的“组件渲染过程”,理论指的是 jsx 代码段;
  • 因为 命令式 的代码,能够应用 try/catch 来解决异样;
  • 但 React 的组件是“申明式”的,开发者无奈在组件内间接应用 try/catch 来解决异样。

而 React 16 带来了 谬误边界 这一全新的概念,向开发者提供一种能力来更精密地解决组件维度抛出的异样。

谬误边界就像一堵 防火墙(命名上也有点像),咱们能够在整个组件树中“搁置”若干这样的“防火墙”,那么一旦某个组件出现异常,该异样会被离它最近的谬误边界给拦挡住,防止影响组件树的其它分支;而咱们也能够通过谬误边界来渲染更“用户敌对”的 UI 界面。

什么样的组件能力被称为谬误边界

谬误边界 也是一个组件(目前只反对 类组件),因而咱们能够插入任意数量的谬误边界到组件树中的任意地位。

谬误边界蕴含两个 API:类组件静态方法 getDerivedStateFromError 和类组件成员办法 componentDidCatch(也能够了解成是生命周期办法),只有一个类组件 (ClassComponent) 蕴含这两者或其中之一,那么这个类组件就成为了谬误边界。

贴一段 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; 
  }
}

谬误边界能达到什么成果

晚期版本的谬误边界只有 componentDidCatch 一个 API,后减少了 getDerivedStateFromError,这两个 API 各司其职。

getDerivedStateFromError

getDerivedStateFromError 的次要性能是在捕捉到异样后,返回以后组件的最新 state。通常的做法就如上文的实例,设置一个 hasError 开关,通过开关来管制是展现“谬误提醒”还是失常的子组件树;这点还是比拟重要的,因为此时子组件树中的某个子组件异样,必须将其从页面上排除进来,否则还是会影响整棵组件树的渲染。

static getDerivedStateFromError(error) {
  // 更新 state 使下一次渲染可能显示降级后的 UI
  return {hasError: true};
}

因为 getDerivedStateFromError 会在 render 阶段被调用,因而不应在此处做任何副作用操作;若有须要,应在 componentDidCatch 生命周期办法中执行相应操作。

componentDidCatch

componentDidCatch 会在 commit 阶段被调用,因而齐全能够用来执行副作用操作,比方上报谬误日志。

在晚期还没有 getDerivedStateFromError 这个 API 的时候,须要在 componentDidCatch 这个生命周期里通过 setState 办法来更新 state,但当初曾经齐全不举荐这么做了,因为通过 getDerivedStateFromError,在 render 阶段就曾经解决好了,何必等到 commit 阶段呢?这块内容在下文会具体介绍。

React 的 render 异样解决机制

之所以优先介绍“谬误边界”,一方面是因为这是间接面向开发者的 API,更好了解;另一方面则是 React 为了实现这样的能力,让 render 异样解决机制变得更简单了,不然间接用 try/catch 捕捉异样后对立解决掉就非常简单粗犷了。

异样是如何产生的

上文中提到,谬误边界解决的是组件渲染过程中抛出的异样,其实这实质上也是 React 的 render 异样解决机制所决定的;而其它诸如事件回调办法、setTimeout/setInterval 等异步办法,因为并不会影响 React 组件树的渲染,因而也就不是 render 异样解决机制的指标了。

什么样的异样会被 render 异样解决机制捕捉

简略来说,类组件的 render 办法、函数组件这样的会在 render 阶段被同步执行的代码,一旦抛出异样就会被 render 的异样解决机制捕捉(无论是否有谬误边界)。举一个理论开发中很常遇到的场景:

function App() {
    return (
        <div>
            <ErrorComponent />
        </div>
    );
}

function ErrorComponent(props) {
    // 父组件并没有传 option 参数,此时就会抛出异样:// Uncaught TypeError: Cannot read properties of undefined (reading 'text')
    return <p>{props.option.text}</p>;
}

React.render(<App />, document.getElementById('app'));

在 React 的 render 过程中,上述两个函数组件先后会被执行,而当执行到 props.foo.text 时就会抛出异样,上面是 <ErrorComponent /> 的 jsx 代码通过转译后的,造成的可执行的 js 代码:

function ErrorComponent(props) {return /*#__PURE__*/Object(react_jsx_dev_runtime__WEBPACK_IMPORTED_MODULE_3__["jsxDEV"])("p", {children: props.foo.text // 抛出异样}, void 0, false, { // debug 相干,可疏忽
    fileName: _jsxFileName,
    lineNumber: 35,
    columnNumber: 10
  }, this);
}

组件自身抛异样的具体位置

以下内容须要你对 React 的 render 过程有肯定的理解,请先浏览《React 源码解析系列 – React 的 render 阶段(二):beginWork》

beginWork 办法中,若判断以后 Fiber 节点无奈 bailout(剪枝),那么就会创立 / 更新 Fiber 子节点:

switch (workInProgress.tag) {
  case IndeterminateComponent: 
    // ... 省略
  case LazyComponent: 
    // ... 省略
  case FunctionComponent: {
      const Component = workInProgress.type;
      const unresolvedProps = workInProgress.pendingProps;
      const resolvedProps =
        workInProgress.elementType === Component
          ? unresolvedProps
          : resolveDefaultProps(Component, unresolvedProps);
      return updateFunctionComponent(
        current,
        workInProgress,
        Component,
        resolvedProps,
        renderLanes,
      );
    }
  case ClassComponent: {
    const Component = workInProgress.type;
    const unresolvedProps = workInProgress.pendingProps;
    const resolvedProps =
      workInProgress.elementType === Component
        ? unresolvedProps
        : resolveDefaultProps(Component, unresolvedProps);
    return updateClassComponent(
      current,
      workInProgress,
      Component,
      resolvedProps,
      renderLanes,
    );
  }
  case HostRoot:
    // ... 省略
  case HostComponent:
    // ... 省略
  case HostText:
    // ... 省略
  // ... 省略其余类型
}
ClassComponent 抛异样的地位

从下面 beginWork 这代码段能够看到执行了 updateClassComponent 办法,并且传入了名为 Component 的参数,此参数实际上就是类组件的 class,此时因为尚未执行 render 办法,因而仍未抛出异样。

循着 updateClassComponent,咱们能够看到执行了 finishClassComponent 来创立 Fiber 子节点:

function updateClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  nextProps: any,
  renderLanes: Lanes,
) {
    // 省略
    const nextUnitOfWork = finishClassComponent(
      current,
      workInProgress,
      Component,
      shouldUpdate,
      hasContext,
      renderLanes,
    );
    // 省略
}

finishClassComponent 中,咱们能够看到 nextChildren = instance.render(),这里的 instance 就是实例化后的类对象,而调用 render 成员办法后,便失去了 nextChildren 这一 ReactElement 对象。

在后续过程中,React 会依据这一 ReactElement 对象来创立 / 更新 Fiber 子节点,但这不是本文所关怀的;咱们关怀的是,这里执行了 render 成员办法,也就有可能抛出 React 异样解决机制所针对的异样。

FunctionComponent 抛异样的地位

接下来咱们来定位与 FunctionComponent 抛异样的地位:有了 ClassComponent 的教训,咱们一路循着 updateFunctionComponentrenderWithHooks,在该办法中,咱们能够看到 let children = Component(props, secondArg);,这里的 Component 就是函数组件的 function 自身,而 children 则是执行函数组件后失去的 ReactElement 对象。

如何捕捉 render 异样

当咱们被问到“如何捕捉异样”,本能就会答复“用 try/catch 呀”,那 React 是如何捕捉这组件渲染过程中抛出的异样的呢?

  • 在生产环境下,React 应用 try/catch 来捕捉异样
  • 在开发环境下,React 没有应用 try/catch,而是实现了一套更为精密的捕捉机制

为什么不能间接应用 try/catch 呢

React 原先就是间接应用 try/catch 来捕捉 render 异样的,后果收到了大量的 issue,详情是这样的:

  • Chrome devtools 有个名为 Pause on exceptions 的性能,该性能能够疾速定位到抛出异样的代码地位,成果就相当于在该行代码上打了断点一样;但只有未被捕捉的异样可能应用这种办法来定位
  • 开发者们投诉无奈通过 Chrome devtools 定位到 React 组件渲染过程中抛出异样的代码
  • 有人发现只有关上 Pause On Caught Exceptions 便能定位到抛出异样的代码地位;这个性能开启后,即使异样被捕捉也能够定位到指标地位,由此判断 React 把异样给“吞”了

为了解决这个问题,React 须要提供一套满足以下条件的异样捕捉计划:

  • 仍然须要捕捉异样,捕捉后交给谬误边界来解决
  • 不应用 try/catch,防止影响 Chrome devtools 的 Pause on exceptions 性能

如何不应用 try/catch 来捕捉 render 异样

当 JavaScript 运行时谬误(包含语法错误)产生时,window 会触发一个 ErrorEvent 接口的 error 事件,并执行 window.onerror()。

上述这段形容出自 MDN GlobalEventHandlers.onerror 的文档,这便是除 try/catch 外的捕捉异样的办法:咱们能够给 window 对象的 error 事件挂上回调解决办法,那么只有页面上任意 javascript 代码抛出异样,咱们都能够捕捉到。

但这样做的话,岂不是也捕捉到许多与 React 组件渲染过程无关的异样?其实,咱们只须要在执行 React 组件渲染前监听 error 事件,而在组件完结渲染后勾销监听该事件即可:

function mockTryCatch(renderFunc, handleError) {window.addEventListener('error', handleError); // handleError 能够了解成是启用“谬误边界”的入口办法
    renderFunc();
    window.removeEventListener('error', handleError); // 革除副作用
}

那是不是这样就功败垂成了呢?且慢!这套计划的原意是要在开发环境取代原先应用的 try/catch 代码,但当初却有一个 try/catch 的重要个性没有还原:那就是 try/catch 在捕捉异样后,会继续执行 try/catch 以外的代码:

try {throw '异样';} catch () {console.log('捕捉到异样!')
}
console.log('持续失常运行');

// 上述代码运行的后果是:// 捕捉到异样!// 持续失常运行

而应用上述的 mockTryCatch 来试图代替 try/catch 的话:

mockTryCatch(() => {throw '异样';}, () => {console.log('捕捉到异样!')
});
console.log('持续失常运行');

// 上述代码运行的后果是:// 捕捉到异样!

不言而喻,mockTryCatch 并不能齐全代替 try/catch,因为 mockTryCatch 在抛出异样后,后续同步代码的执行就会被强制终止。

如何像 try/catch 一样不影响后续代码执行

前端畛域总是有各种各样的骚套路,还真让 React 开发者找到这样的办法:EventTarget.dispatchEvent;那么,为什么说 dispatchEvent 就能模仿并代替 try/catch 呢?

dispatchEvent 可能同步执行代码

与浏览器原生事件不同,原生事件是由 DOM 派发的,并通过 event loop 异步调用事件处理程序,而 dispatchEvent() 则是同步调用事件处理程序。在调用 dispatchEvent() 后,所有监听该事件的事件处理程序将在代码持续前执行并返回。

上文出自 dispatchEvent 的 MDN 文档,由此可见:dispatchEvent 可能同步执行代码,这意味着在事件处理办法执行实现前,能够阻塞 dispatchEvent 后续的代码执行,同时这也是 try/catch 的特色之一。

dispatchEvent 抛的异样不冒泡

这些 event handlers 运行在一个嵌套的调用栈中:他们会阻塞调用直到他们处理完毕,然而异样不会冒泡。

精确来说,是:通过 dispatchEvent 触发的事件回调办法,异样不会冒泡;这意味着,即使抛出异样,也只是会终止事件回调办法自身的执行,而 dispatchEvent() 上下文的代码并不会收到影响。上面写个 DEMO 验证下这个个性:

function cb() {console.log('开始执行回调');
  throw 'dispatchEvent 的事件处理函数抛异样了';
  console.log('走不到这里的');
}

/* 筹备一个虚构事件 */
const eventType = 'this-is-a-custom-event';
const fakeEvent = document.createEvent('Event');
fakeEvent.initEvent(eventType, false, false);

/* 筹备一个虚构 DOM 节点 */
const fakeNode = document.createElement('fake');
fakeNode.addEventListener(eventType, cb, false); // 挂载

console.log('dispatchEvent 执行前');
fakeNode.dispatchEvent(fakeEvent);
console.log('dispatchEvent 执行后');

// 上述代码运行的后果是:// dispatchEvent 执行前
// 开始执行回调
// Uncaught dispatchEvent 的事件处理函数抛异样了
// dispatchEvent 执行后

从上述 DEMO 能够看出,只管 dispatchEvent 的事件处理函数抛了异样,但仍然还是可能继续执行 dispatchEvent 后续的代码(即 DEMO 中的 console.log())。

实现一个简易版的 render 异样捕捉器

接下来,让咱们把 GlobalEventHandlers.onerrorEventTarget.dispatchEvent 联合起来,就可能实现一个简易版的 render 异样捕捉器:

function exceptionCatcher(func) {
    /* 筹备一个虚构事件 */
    const eventType = 'this-is-a-custom-event';
    const fakeEvent = document.createEvent('Event');
    fakeEvent.initEvent(eventType, false, false);

    /* 筹备一个虚构 DOM 节点 */
    const fakeNode = document.createElement('fake');
    fakeNode.addEventListener(eventType, excuteFunc, false); // 挂载

    window.addEventListener('error', handleError);
    fakeNode.dispatchEvent(fakeEvent); // 触发执行指标办法
    window.addEventListener('error', handleError); // 革除副作用
    
    function excuteFunc() {func();
        fakeNode.removeEventListener(evtType, excuteFunc, false); 
    }
    
    function handleError() {// 将异样交给谬误边界来解决}
}

React 源码中具体是如何捕捉 render 异样的

上文介绍完捕捉 render 异样的原理,也实现了个简易版 DEMO,上面就能够来具体分析 React 源码了。

捕捉指标:beginWork

上文提到,React 组件渲染维度的异样是在 beginWork 阶段抛出,因而咱们捕捉异样的指标显然就是 beginWork 了。

对 beginWork 进行包装

React 针对开发环境对 beginWork 办法进行了一个封装,添上了 捕捉异样 的性能:

  1. 在执行 bginWork 前,先“备份”一下以后的 Fiber 节点 (unitOfWork) 的属性,复制到一个专门用于“备份”的 Fiber 节点上。
  2. 执行 beginWork 并应用 try/catch 捕捉异样;看到这你兴许会很纳闷,不是说不必 try/catch 来捕捉异样吗,这怎么又用上了?还是持续往下看吧。
  3. 若 beginWork 抛出了异样,天然就会被捕捉到,而后执行 catch 的代码段:

    1. 从备份中复原以后 Fiber 节点 (unitOfWork) 到执行 beginWork 前的状态。
    2. 在以后 Fiber 节点上调用 invokeGuardedCallback 办法来从新执行一遍 beginWork,这个 invokeGuardedCallback 办法会利用咱们上文中提到的 GlobalEventHandlers.onerrorEventTarget.dispatchEvent 联结办法来捕捉异样。
    3. 从新抛出捕捉到的异样,后续能够针对异样进行解决;这里尽管抛出异样,并且这个异样会被外层的 try/catch 给捕捉,但这不会影响 Pause on exceptions 性能,因为 invokeGuardedCallback 办法内产生的异样,并没有被外层的 try/catch 捕捉。

let beginWork;
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
    // 开发环境会走到这个分支
    beginWork = (current, unitOfWork, lanes) => {
        /*
            把以后 Fiber 节点 (unitOfWork) 的所有属性,拷贝到一个额定的 Fiber 节点(dummyFiber)中
            这个 dummyFiber 节点仅仅作为备份应用,并且永远不会被插入到 Fiber 树中
         */
        const originalWorkInProgressCopy = assignFiberPropertiesInDEV(
          dummyFiber,
          unitOfWork,
        );
        try {return originalBeginWork(current, unitOfWork, lanes); // 执行真正的 beginWork 办法
        } catch (originalError) {
            // ... 省略
            
            // 从备份中复原以后 Fiber 节点 (unitOfWork) 到执行 beginWork 前的状态
            assignFiberPropertiesInDEV(unitOfWork, originalWorkInProgressCopy);
            
            // ... 省略
            
            // 从新在以后 Fiber 节点上执行一遍 beginWork,这里是本文介绍捕捉异样的重点
            invokeGuardedCallback(
              null,
              originalBeginWork,
              null,
              current,
              unitOfWork,
              lanes,
            );

            // 从新抛出捕捉到的异样,后续能够针对异样进行解决,下文会介绍
        }
    };
} else {
    // 生产环境会走到这个分支
    beginWork = originalBeginWork;
}
invokeGuardedCallback

接下来看 invokeGuardedCallback 办法,这个办法其实并非外围,它跟它所在的 ReactErrorUtils.js 文件内的其它办法,造成了一个“存 / 取”异样的工具,咱们关注的外围在 invokeGuardedCallbackImpl 办法。

let hasError: boolean = false;
let caughtError: mixed = null;

const reporter = {onError(error: mixed) {
    hasError = true;
    caughtError = error;
  },
};

export function invokeGuardedCallback<A, B, C, D, E, F, Context>(
  name: string | null,
  func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
  context: Context,
  a: A,
  b: B,
  c: C,
  d: D,
  e: E,
  f: F,
): void {
  hasError = false;
  caughtError = null;
  invokeGuardedCallbackImpl.apply(reporter, arguments);
}
invokeGuardedCallbackImpl

这个 invokeGuardedCallbackImpl 也分生产环境和开发环境的实现,咱们只看开发环境的实现即可:

invokeGuardedCallbackImpl = function invokeGuardedCallbackDev<
  A,
  B,
  C,
  D,
  E,
  F,
  Context,
>(
  name: string | null,
  func: (a: A, b: B, c: C, d: D, e: E, f: F) => mixed,
  context: Context,
  a: A,
  b: B,
  c: C,
  d: D,
  e: E,
  f: F,
) {
  // 省略...
  
  const evt = document.createEvent('Event'); // 创立自定义事件

  // 省略...

  const windowEvent = window.event;

  // 省略...

  function restoreAfterDispatch() {fakeNode.removeEventListener(evtType, callCallback, false); // 勾销自定义事件的监听,革除副作用
    // 省略...
  }

  const funcArgs = Array.prototype.slice.call(arguments, 3); // 取出须要传给 beginWork 的参数
  function callCallback() {
    // 省略...
    restoreAfterDispatch();
    func.apply(context, funcArgs); // 执行 beginWork
    // 省略...
  }

  function handleWindowError(event) {
    error = event.error; // 捕捉到异样
    // 省略...
  }

  // 自定义事件名称
  const evtType = `react-${name ? name : 'invokeguardedcallback'}`;

  window.addEventListener('error', handleWindowError);
  fakeNode.addEventListener(evtType, callCallback, false);

  evt.initEvent(evtType, false, false); // 初始化一个自定义事件
  fakeNode.dispatchEvent(evt); // 触发自定义事件,也能够认为是触发同步执行 beginWork

  // 省略...
  this.onError(error); // 将捕捉到的异样交给外层解决

  window.removeEventListener('error', handleWindowError); // 勾销 error 事件的监听,革除副作用
};

以上是我精简后的 invokeGuardedCallbackImpl 办法,是不是跟咱们上述实现的简易版 React 异样捕捉器相差无几呢?当然,该办法内其实还包含了很多异常情况的解决,这些异常情况都是由 issues 提出,而后以“打补丁”的形式来解决的,例如在测试环境中缺失 document,又或是碰到跨域异样 (cross-origin error) 等,这里就不一一细说了。

解决异样

上文介绍了异样是怎么产生的,也介绍了异样是怎么被捕捉的,上面就来简略介绍一下异样被捕捉到后是怎么解决的:

  1. 从抛异样的 Fiber 节点开始,往根节点方向遍历,寻找能解决本次异样的谬误边界;如果找不到,就只能交给根节点来解决异样。
  2. 如果由谬误边界来解决异样,则创立一个 payloadgetDerivedStateFromError 办法执行后返回的 state 值、callbackcomponentDidCatch 的更新工作;如果是由根节点来解决异样,则创立一个卸载整个组件树的更新工作。
  3. 进入解决异样的节点的 render 过程中(也即 performUnitOfWork),在该过程中会执行刚刚创立的更新工作。
  4. 最终,如果由谬误边界来解决异样,那么依据谬误边界 state 的变动,会卸载掉带有异样 Fiber 节点的子组件树,改为渲染含有敌对异样提醒的 UI 界面;而如果由根节点来解决异样,则会卸载掉整个组件树,导致白屏。

React 中解决异样的源码实现

上文说到在(开发环境)封装的 beginWork 里,会把 invokeGuardedCallback 捕捉到的异样从新抛出,那这个异样会在哪里被截住呢?答案是 renderRootSync

do {
  try {workLoopSync(); // workLoopSync 中会调用 beginWork
    break;
  } catch (thrownValue) {handleError(root, thrownValue); // 解决异样
  }
} while (true);
handleError

上面来介绍 handleError

  • handleError 又是一个 React 习用的 do...while(true) 的死循环构造,那么满足什么条件能力退出循环呢?
  • 在循环体内,有一个 try/catch 代码段,一旦 try 中的代码段抛异样被 catch 拦挡住,那么就会回退到以后节点的父节点(React 的老套路了)持续尝试;如果某次执行中未抛异样,就能完结该循环,也即完结整个 handleError 办法。
  • try 代码段中,次要执行了 3 段逻辑:

    1. 判断以后节点或以后节点的父节点是否为 null,如果是的话,则表明以后可能处在 Fiber 根节点,不可能有谬误边界可能解决异样,间接作为致命异样来解决,完结以后办法。
    2. 执行 throwException 办法,遍历寻找一个能解决以后异样的节点(谬误边界),下文将具体介绍。
    3. 执行 completeUnitOfWork,这是 render 过程中最重要的办法之一,但这里次要是执行其中对于异样解决的代码分支,下文将具体介绍。

function handleError(root, thrownValue): void {
  do {
    let erroredWork = workInProgress;
    try {
      // 重置 render 过程中批改过的一些状态,省略...

      if (erroredWork === null || erroredWork.return === null) {
        // 若走到这个分支,则表明以后可能处在 Fiber 根节点,不可能有谬误边界可能解决异样,间接作为致命异样来解决
        workInProgressRootExitStatus = RootFatalErrored;
        workInProgressRootFatalError = thrownValue;
        workInProgress = null;
        return;
      }

      // 省略...
      
      // throwException 是关键所在,下文介绍
      throwException(
        root,
        erroredWork.return,
        erroredWork,
        thrownValue, // 异样自身
        workInProgressRootRenderLanes,
      );
      completeUnitOfWork(erroredWork); // 解决异样的 Fiber 节点(erroredWork)
    } catch (yetAnotherThrownValue) {// 如果上述代码段仍然无奈解决以后异样 Fiber 节点 (erroredWork) —— 还是抛了异样,那么就尝试用异样节点(erroredWork) 的 Fiber 父节点来解决
      // 这是一个循环过程,始终向上遍历父级节点,直到找到能够解决异样的 Fiber 节点,又或是达到 Fiber 根节点(确定无谬误边界可能解决以后异样)thrownValue = yetAnotherThrownValue;
      if (workInProgress === erroredWork && erroredWork !== null) {
        erroredWork = erroredWork.return;
        workInProgress = erroredWork;
      } else {erroredWork = workInProgress;}
      continue;
    }
    return;
  } while (true);
}
throwException

上面来介绍 throwExceptionthrowException 次要做了以下事件:

  • 给以后抛异样的 Fiber 节点打上 Incomplete 这个 EffectTag,后续会依据这个 Incomplete 标识走到异样解决的代码分支里。
  • 从以后抛异样的 Fiber 节点的父节点开始,往根节点方向遍历,找一个能够解决异样的节点;目前只有谬误边界和 Fiber 根节点能够解决异样;依据遍历的方向,如果这个遍历门路中有谬误边界的话,必定会先找到谬误边界,也就是优先让谬误边界来解决异样。

    • 判断谬误边界的规范”在这里就能够体现:必须是一个 ClassComponent,且蕴含 getDerivedStateFromErrorcomponentDidCatch 两者或其中之一。
  • 找到能够解决异样的节点后,也会依据不同的类型来执行不同的代码分支,不过大略思路是一样的:

    1. 给该节点打上 ShouldCapture 的 EffectTag,后续会依据这个 EffectTag 走到异样解决的代码分支。
    2. 针对以后异样新建一个更新工作,并给该更新工作找一个优先级最高的 lane,保障在本次 render 时必定会执行;其中,谬误边界会调用 createRootErrorUpdate 办法来创立更新工作,而根节点则是调用 createRootErrorUpdate 办法来创立更新工作,这两个办法下文都会具体介绍的。
function throwException(
  root: FiberRoot,
  returnFiber: Fiber,
  sourceFiber: Fiber,
  value: mixed, // 异样自身
  rootRenderLanes: Lanes,
) {
  // 给以后异样的 Fiber 节点打上 Incomplete 这个 EffectTag,后续就依据这个 Incomplete 标识走到异样解决的代码分支里
  sourceFiber.effectTag |= Incomplete;
  sourceFiber.firstEffect = sourceFiber.lastEffect = null;

  // 一大段针对 Suspense 场景的解决,省略...

  renderDidError(); // 将 workInProgressRootExitStatus 置为 RootErrored

  value = createCapturedValue(value, sourceFiber); // 获取从 Fiber 根节点到异样节点的残缺节点门路,挂载到异样上,不便后续打印
  /*
    尝试往异样节点的父节点方向遍历,找一个能够解决异样的谬误边界,如果找不到的话那就只能交给根节点来解决了
    留神,这里并不是从异样节点开始找的,因而即使异样节点本人是谬误边界,也不能解决以后异样
   */
  let workInProgress = returnFiber;
  do {switch (workInProgress.tag) {
      case HostRoot: {
        // 进到这个代码分支意味着没能找到一个可能解决本次异样的谬误边界,只能让 Fiber 根节点来解决异样
        // 给该节点打上 ShouldCapture 的 EffectTag,后续会依据这个 EffectTag 走到异样解决的代码分支
        const errorInfo = value;
        workInProgress.effectTag |= ShouldCapture;
        // 针对以后异样新建一个更新工作,并给该更新工作找一个优先级最高的 lane,保障在本次 render 时必定会执行
        const lane = pickArbitraryLane(rootRenderLanes);
        workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
        const update = createRootErrorUpdate(workInProgress, errorInfo, lane); // 要害,下文会介绍
        enqueueCapturedUpdate(workInProgress, update);
        return;
      }
      case ClassComponent:
        const errorInfo = value;
        const ctor = workInProgress.type;
        const instance = workInProgress.stateNode;
        
        // 判断该节点是否为谬误边界
        if ((workInProgress.effectTag & DidCapture) === NoEffect &&
          (typeof ctor.getDerivedStateFromError === 'function' ||
            (instance !== null &&
              typeof instance.componentDidCatch === 'function' &&
              !isAlreadyFailedLegacyErrorBoundary(instance)))
        ) {
          // 确定该节点是谬误边界
          // 给该节点打上 ShouldCapture 的 EffectTag,后续会依据这个 EffectTag 走到异样解决的代码分支
          workInProgress.effectTag |= ShouldCapture; 
          // 针对以后异样新建一个更新工作,并给该更新工作找一个优先级最高的 lane,保障在本次 render 时必定会执行
          const lane = pickArbitraryLane(rootRenderLanes);
          workInProgress.lanes = mergeLanes(workInProgress.lanes, lane);
          const update = createClassErrorUpdate( // 要害,下文会介绍
            workInProgress,
            errorInfo,
            lane,
          );
          enqueueCapturedUpdate(workInProgress, update);
          return;
        }
        break;
      default:
        break;
    }
    workInProgress = workInProgress.return;
  } while (workInProgress !== null);
}
createRootErrorUpdate 和 createClassErrorUpdate

当遇到无谬误边界能解决的致命异样时,会调用 createRootErrorUpdate 办法来创立一个状态更新工作,该工作会将根节点置为 null,即卸载整棵 React 组件树。React 官网认为,与其渲染一个异样的界面误导用户,还不如间接显示白屏;我无奈否定官网的这种思维,但更必定了谬误边界的重要性。

function createRootErrorUpdate(
  fiber: Fiber,
  errorInfo: CapturedValue<mixed>,
  lane: Lane,
): Update<mixed> {const update = createUpdate(NoTimestamp, lane, null);
  update.tag = CaptureUpdate;
  // 将根节点置为 null,即卸载整棵 React 组件树
  update.payload = {element: null};
  const error = errorInfo.value;
  update.callback = () => {
    // 打印错误信息
    onUncaughtError(error);
    logCapturedError(fiber, errorInfo);
  };
  return update;
}

当发现有谬误边界能够解决以后异样时,会调用 createClassErrorUpdate 办法来创立一个状态更新工作,该更新工作的 payloadgetDerivedStateFromError 执行后返回的后果,而在更新工作的 callback 中,则执行了 componentDidCatch 办法(通常用来执行一些带有副作用的操作)。

function createClassErrorUpdate(
  fiber: Fiber,
  errorInfo: CapturedValue<mixed>,
  lane: Lane,
): Update<mixed> {const update = createUpdate(NoTimestamp, lane, null);
  update.tag = CaptureUpdate;
  // 留神这里的 getDerivedStateFromError 是取类组件自身的静态方法
  const getDerivedStateFromError = fiber.type.getDerivedStateFromError;
  if (typeof getDerivedStateFromError === 'function') {
    const error = errorInfo.value;
    // 在新创建的状态更新工作中,将 state 设置为 getDerivedStateFromError 办法执行后返回的后果
    update.payload = () => {logCapturedError(fiber, errorInfo);
      return getDerivedStateFromError(error);
    };
  }

  const inst = fiber.stateNode;
  if (inst !== null && typeof inst.componentDidCatch === 'function') {
    // 设置更新工作的 callback
    update.callback = function callback() {
      // 省略...
      if (typeof getDerivedStateFromError !== 'function') {
        // 兼容晚期的谬误边界版本,过后并没有 getDerivedStateFromError 这个 API
        // 省略...
      }
      const error = errorInfo.value;
      const stack = errorInfo.stack;
      // 执行类组件的 componentDidCatch 成员办法,通常用来执行一些带有副作用的操作
      this.componentDidCatch(error, {componentStack: stack !== null ? stack : '',});
      // 省略...
    };
  }
  // 省略...
  return update;
}
completeUnitOfWork

下面讲完了 throwException,上面持续看 handleError 办法中的最初一个步骤 —— completeUnitOfWork,该办法会对异样的 Fiber 节点进行解决,在异样场景中该办法的惟一参数是 抛出异样的 Fiber 节点

function handleError(root, thrownValue): void {
  do {
    let erroredWork = workInProgress;
    try {
      // 省略...
      
      // throwException 是关键所在,下文介绍
      throwException(
        root,
        erroredWork.return,
        erroredWork,
        thrownValue, // 异样自身
        workInProgressRootRenderLanes,
      );
      completeUnitOfWork(erroredWork); // 抛出异样的 Fiber 节点(erroredWork)
    } catch (yetAnotherThrownValue) {// 省略...}
    return;
  } while (true);
}

在之前的文章中,咱们曾经介绍过 completeUnitOfWork 办法了,但介绍的是失常的流程,间接把异样解决的流程给疏忽了,上面咱们来补上这一块:

  • completeUnitOfWork 跟上文介绍的 throwException 有点像,是从以后 Fiber 节点(在异样场景指的是抛异样的节点)往根节点方向遍历,找一个能够解决异样的节点;因为 completeUnitOfWork 同时蕴含了失常流程和异样解决流程,因而是通过 Incomplete 这个 EffectTag 来进入到异样解决的代码分支里的。
  • 一旦发现能够解决异样的 Fiber 节点,则将其设置为下一轮 work(performUnitOfWork)循环主体(workInProgres),而后立刻终止本 completeUnitOfWork 办法;后续就会回到 performUnitOfWork 并进入到该(能够解决异样的)Fiber 节点的 beginWork 阶段。
  • 在遍历过程中,如果发现以后节点无奈解决异样,那么就会给以后节点的父节点也打上 Incomplete,保障父节点也会进入到异样解决的代码分支。
  • completeUnitOfWork 中针对 sibling 节点的逻辑并没有辨别是否为失常流程,这点我有点意外:因为如果以后节点有异样,那么它的 sibling 节点即使是失常的,在后续的异样处理过程中也会被从新 render,此时又何必去 render 它的 sibling 节点呢;但反过来想,这样做也不会产生问题,因为 sibling 节点在 completeUnitOfWork 回退到父节点时,因为父节点曾经被设置为 Incomplete 了,所以也仍然会走异样解决的流程。

这里还有个问题:为什么要从新 render 能够解决异样的节点 呢?咱们不看后续的操作其实就能猜到 React 的做法:假如这个 能够解决异样的节点 是一个谬误边界,在上文介绍的 throwException 中曾经依据 getDerivedStateFromError 执行后返回的 state 值来创立了一个更新工作,那么后续只须要更新谬误边界的 state,依据 state 卸载掉抛异样的组件并渲染谬误提醒的组件,那这不就是一个很失常的 render 流程了吗。

function completeUnitOfWork(unitOfWork: Fiber): void {
  let completedWork = unitOfWork; // 这里的 unitOfWork 指的是抛出异样的 Fiber 节点
  do {
    const current = completedWork.alternate;
    const returnFiber = completedWork.return;

    // 判断以后 Fiber 节点是否被打上 Incomplete 这个 EffectTag
    if ((completedWork.effectTag & Incomplete) === NoEffect) {// 失常的 Fiber 节点的解决流程,省略...} else {
      // 以后 Fiber 节点是否被打上 Incomplete 这个 EffectTag,即以后 Fiber 节点因为异样,未能实现 render 过程,尝试走进解决异样的流程

      // 判断以后 Fiber 节点 (completeWork) 是否解决异样,如果能够的话就赋给 next 变量
      const next = unwindWork(completedWork, subtreeRenderLanes);

      // Because this fiber did not complete, don't reset its expiration time.

      if (next !== null) {// 发现以后 Fiber 节点可能解决异样,将其设置为下一轮 work(performUnitOfWork)的循环主体(workInProgres),// 而后立刻终止以后的 completeWork 阶段,后续将进入到以后 Fiber 节点的 beginWork 阶段(render 的“递”阶段)
        next.effectTag &= HostEffectMask;
        workInProgress = next;
        return;
      }

      // 省略...

      // 走到这个分支意味着以后 Fiber 节点 (completeWork) 并不能解决异样,// 因而把 Fiber 父节点也打上 Incomplete 的 EffectTag,后续将持续尝试走进解决异样的流程
      if (returnFiber !== null) {
        // Mark the parent fiber as incomplete and clear its effect list.
        returnFiber.firstEffect = returnFiber.lastEffect = null;
        returnFiber.effectTag |= Incomplete;
      }
    }

    // 解决以后 Fiber 节点的 sibling 节点,能够失常进入 sibling 节点的 beginWork 阶段
    // 后续会持续通过 sibling 节点的 completeUnitOfWork 回退到父节点来判断是否可能解决异样
    
    // 在以后循环中回退到父节点,持续尝试走进解决异样的流程
    completedWork = returnFiber;
    workInProgress = completedWork;
  } while (completedWork !== null);
  // 省略...
}
unwindWork

这里介绍一下 unwindWork 办法是怎么判断以后 Fiber 节点 (completeWork) 是否解决异样的:

  • 依据 completeWork.tag 即 Fiber 节点类型来判断,仅有 ClassComponent / HostRoot / SuspenseComponent / DehydratedSuspenseComponent 这 4 类 Fiber 节点类型可能解决异样
  • 依据 completeWork.effectTag 中是否蕴含 ShouldCapture 来判断,这个 EffectTag 是在上文介绍的 throwException 办法打上的。

unwindWork 办法中,一旦判断以后 Fiber 节点可能解决异样,那么则革除其 ShouldCapture,并添上 DidCapture 的 EffectTag,该 EffectTag 也会成为后续异样解决的判断规范。

function unwindWork(workInProgress: Fiber, renderLanes: Lanes) {switch (workInProgress.tag) {
    case ClassComponent: {
      // 省略...
      const effectTag = workInProgress.effectTag;
      // 判断是否蕴含 ShouldCapture 这个 EffectTag
      if (effectTag & ShouldCapture) {
        // 确定以后 Fiber 节点可能解决异样,即确定为谬误边界
        // 革除以后 Fiber 节点的 ShouldCapture,并添上 DidCapture 的 EffectTag 
        workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
        // 省略...
        return workInProgress;
      }
      return null;
    }
    case HostRoot: {
      // 进到以后代码分支,意味着在以后 Fiber 树中没有可能解决本次异样的谬误边界
      // 因而交由 Fiber 根节点来对立解决异样
      // 省略...
      const effectTag = workInProgress.effectTag;
      // 省略...
      // 革除 Fiber 根节点的 ShouldCapture,并添上 DidCapture 的 EffectTag 
      workInProgress.effectTag = (effectTag & ~ShouldCapture) | DidCapture;
      return workInProgress;
    }
    // 省略...
    default:
      return null;
  }
}
从新 render 谬误边界 Fiber 节点

在 completeUnitOfWork 办法中,咱们通过 do...while 循环配合 unwindWork 办法,寻找在 throwException 办法中曾经标记过能够解决以后异样的 谬误边界 节点;上面假如确实有这样的一个 谬误边界 节点,那么 completeUnitOfWork 办法会被完结,而后就进入到该节点的第二次 render:

workLoopSync --> performUnitOfWork --> beginWork --> updateClassComponent -> updateClassInstance / finishClassComponent

下面这都是失常 render 一个 ClassComponent 的过程,首先咱们须要关注到 updateClassInstance,在这个办法中,会针对以后节点的更新工作,来更新节点的 state;还记得在 createClassErrorUpdate 中依据类组件静态方法 getDerivedStateFromError 返回的 state 值来创立的一个更新工作吗,该更新工作还被赋予了最高优先级:pickArbitraryLane(rootRenderLanes),因而在 updateClassInstance 就会依据这个更新工作来更新 state(也就是 getDerivedStateFromError 返回的 state 值)。

而后,咱们进入到 finishClassComponent 办法的逻辑里,本办法针对异样解决其实就做了两个事件:

  1. 兼容老版谬误边界的 API

    • 判断是否为老版谬误边界的根据是:以后节点的 ClassComponent 是否存在 getDerivedStateFromError 这个类静态方法;在老版谬误边界中,没有 getDerivedStateFromError 这个 API,对立是在 componentDidCatch 中发动 setState() 来批改 state 的,
    • 兼容的办法是:在本次 render 过程中,把 nextChildren 设置为 null,即卸载掉所有的子节点,这样的话就能防止本次 render 抛异样;而在 commit 阶段,会执行更新工作的 callback,即 componentDidCatch,到时候能够发动新一轮 render。
  2. 强制从新创立子节点,这块其实与失常逻辑调用 reconcileChildren 差异不大,但做了一些小伎俩来禁止复用 current 树上的子节点,下文会具体介绍。
function finishClassComponent(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: any,
  shouldUpdate: boolean,
  hasContext: boolean,
  renderLanes: Lanes,
) {
  // 省略...
  // 判断是否有 DidCapture 这个 EffectTag,若带有该 EffectTag,则示意以后 Fiber 节点为解决异样的谬误边界
  const didCaptureError = (workInProgress.effectTag & DidCapture) !== NoEffect;

  // 失常的 render 流程代码分支,省略...

  const instance = workInProgress.stateNode;
  ReactCurrentOwner.current = workInProgress;
  let nextChildren;
  if (
    didCaptureError &&
    typeof Component.getDerivedStateFromError !== 'function'
  ) {
    // 若以后为解决异样的谬误边界,但又没有定义 getDerivedStateFromError 这办法,则进入到本代码分支
    // 这个代码分支次要是为了兼容老版谬误边界的 API,在老版谬误边界中,是在 componentDidCatch 发动 setState()来批改 state 的
    // 兼容的办法是,在本次 render 过程中,把 nextChildren 设置为 null,即卸载掉所有的子节点,这样的话就能防止本次 render 抛异样
    nextChildren = null;
    // 省略...
  } else {// 失常的 render 流程代码分支,省略...}

  // 省略...
  
  if (current !== null && didCaptureError) {
    // 强制从新创立子节点,禁止复用 current 树上的子节点;forceUnmountCurrentAndReconcile(
      current,
      workInProgress,
      nextChildren,
      renderLanes,
    );
  } else {
    // 失常的 render 流程代码分支
    reconcileChildren(current, workInProgress, nextChildren, renderLanes);
  }

  // 省略...

  return workInProgress.child;
}
如何强制从新渲染子节点

在介绍 finishClassComponent 时咱们提到能够用 forceUnmountCurrentAndReconcile 办法,与失常的 render 逻辑相似,该办法中也会调用 reconcileChildFibers,但却十分奇妙地调用了两次:

  1. 第一次调用 reconcileChildFibers 时,会把本来应该传“子节点 ReactElement 对象 ”的参数改为传 null,相当于卸载掉所有子节点;这样的话就会给 current 树上的所有子节点都标记上“ 删除”的 EffectTag。
  2. 第二次调用 reconcileChildFibers 时,会把本来应该传“current 树上对应子节点”的参数改为传 null;这样的话就能保障本次 render 后,以后节点(谬误边界)的所有子节点都是新创建的,不会复用 current 树节点

至于为什么要这么做呢,React 官网的解释是“从概念上来说,解决异样时与失常渲染时是不同的两套 UI,不应该复用任何子节点(即便该节点的特色 —— key/props 等是统一的)”;简略来了解的话,就是“一刀切”防止复用到异样的 Fiber 节点吧。

function forceUnmountCurrentAndReconcile(
  current: Fiber,
  workInProgress: Fiber,
  nextChildren: any,
  renderLanes: Lanes,
) {
  // 只有在 render 解决异样的谬误边界时,才会进入到以后办法;当然失常逻辑下也是会执行 reconcileChildFibers
  // 在解决异样时,应该回绝复用 current 树上对应的 current 子节点,防止复用到异样的子节点;为此,会调用两次 reconcileChildFibers
  
  // 第一次调用 reconcileChildFibers,会把本来应该传子节点 ReactElement 对象的参数改为传 null
  // 这样的话就会给 current 树上的所有子节点都标记上“删除”的 EffectTag
  workInProgress.child = reconcileChildFibers(
    workInProgress,
    current.child,
    null,
    renderLanes,
  );

  // 第二次调用 reconcileChildFibers,会把本来应该传 current 树上对应子节点的参数改为传 null
  // 这样就能保障本次 render 后的所有子节点都是新创建的,不会复用
  workInProgress.child = reconcileChildFibers(
    workInProgress,
    null,
    nextChildren,
    renderLanes,
  );
}

写在最初

以上便是对 React render 异样解决机制的介绍,通过本文,补全了后面几篇介绍 render 的文章的疏漏(前文仅介绍了 render 的失常流程),让咱们在开发过程中做到对异样解决“心里有数”,快给你的利用加几个谬误边界吧(笑)。

正文完
 0