27

不用try catch,如何机智的捕获错误

 4 years ago
source link: http://developer.51cto.com/art/202009/627319.htm
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

EZVZJrV.jpg!mobile

起源

我们知道,React中有个特性Error Boundary,帮助我们在组件发生错误时显示“错误状态”的UI。

为了实现这个特性,就一定需要捕获到错误。

所以在React源码中,所有用户代码都被包裹在一个方法中执行。

类似如下:

function wrapper(func) { 
  try { 
    func(); 
  } catch(e) { 
    // ...处理错误 
  } 
} 

比如触发componentDidMount时:

wrapper(componentDidMount); 

本来一切都很完美,但是React作为世界级前端框架,受众广泛,凡事都讲究做到极致。

这不,有人提issue:

你们这样在try catch中执行用户代码会让浏览器调试工具的Pause on exceptions失效。 

Pause on exceptions失效的来龙去脉

Pause on exceptions是什么?

他是浏览器调试工具source面板的一个功能。

ZRRFbe3.png!mobile

开启该功能后,在运行时遇到会抛出错误的代码,代码的执行会自动停在该行,就像在该行打了断点一样。

比如,执行如下代码,并开启该功能:

let a = c; 

代码的执行会在该行暂停。

eeMzIfr.png!mobile

这个功能可以很方便的帮我们发现未捕获的错误发生的位置。

但是,当React将用户代码包裹在try catch后,即使代码抛出错误,也会被catch。

Pause on exceptions无法在抛出错误的用户代码处暂停,因为error已经被React catch了。

除非我们进一步开启Pause on caught exceptions。

MZVfmiU.png!mobile

开启该功能,使代码在捕获的错误发生的位置暂停。

如何解决

对用户来说,我写在componentDidMount中的代码明明未捕获错误,可是错误发生时Pause on exceptions却失效了,确实有些让人困惑。

所以,在生产环境,React继续使用try catch实现wrapper。

而在开发环境,为了更好的调试体验,需要重新实现一套try catch机制,包含如下功能:

  • 捕获用户代码抛出的错误,使Error Boundary功能正常运行
  • 不捕获用户代码抛出的错误,使Pause on exceptions不失效

这看似矛盾的功能,React如何机智的实现呢?

如何“捕获”错误

让我们先实现第一点:捕获用户代码抛出的错误。

但是不能使用try catch,因为这会让Pause on exceptions失效。

解决办法是:监听window的error事件。

根据GlobalEventHandlers.onerror MDN[1],该事件可以监听到两类错误:

  • js运行时错误(包括语法错误)。window会触发ErrorEvent接口的error事件
  • 资源(如<img>或<script>)加载失败错误。加载资源的元素会触发Event接口的error事件,可以在window上捕获该错误

实现开发环境使用的wrapperDev:

// 开发环境wrapper 
function wrapperDev(func) { 
  function handleWindowError(error) { 
    // 收集错误交给Error Boundary处理 
  } 
 
  window.addEventListener('error', handleWindowError); 
  func(); 
  window.removeEventListener('error', handleWindowError); 
} 

当func执行时抛出错误,会被handleWindowError处理。

但是,对比生产环境wrapperPrd内func抛出的错误会被catch,不会影响后续代码执行。

function wrapperPrd(func) { 
  try { 
    func(); 
  } catch(e) { 
    // ...处理错误 
  } 
} 

开发环境func内如果抛出错误,代码的执行会中断。

比如执行如下代码,finish会被打印。

wrapperPrd(() => {throw Error(123)}) 
console.log('finish'); 

但是执行如下代码,代码执行中断,finish不会被打印。

wrapperDev(() => {throw Error(123)}) 
console.log('finish'); 

如何在不捕获用户代码抛出错误的前提下,又能让后续代码的执行不中断呢?

如何让代码执行不中断

答案是:通过dispatchEvent触发事件回调,在回调中调用用户代码。

根据EventTarget.dispatchEvent MDN[2]:

不同于DOM节点触发的事件(比如click事件)回调是由event loop异步触发。

通过dispatchEvent触发的事件是同步触发,并且在事件回调中抛出的错误不会影响dispatchEvent的调用者(caller)。

让我们继续改造wrapperDev。

首先创建虚构的DOM节点、事件对象、虚构的事件类型:

// 创建虚构的DOM节点 
const fakeNode = document.createElement('fake'); 
// 创建event 
const event = document.createEvent('Event'); 
// 创建虚构的event类型 
const evtType = 'fake-event'; 

初始化事件对象,监听事件。在事件回调中调用用户代码。触发事件:

function callCallback() { 
  fakeNode.removeEventListener(evtType, callCallback, false);  
  func(); 
} 
 
// 监听虚构的事件类型 
fakeNode.addEventListener(evtType, callCallback, false); 
 
// 初始化事件 
event.initEvent(evtType, false, false); 
 
// 触发事件 
fakeNode.dispatchEvent(event); 

完整流程如下:

function wrapperDev(func) { 
  function handleWindowError(error) { 
    // 收集错误交给Error Boundary处理 
  } 
   
  function callCallback() { 
    fakeNode.removeEventListener(evtType, callCallback, false);  
    func(); 
  } 
   
  const event = document.createEvent('Event'); 
  const fakeNode = document.createElement('fake'); 
  const evtType = 'fake-event'; 
 
  window.addEventListener('error', handleWindowError); 
  fakeNode.addEventListener(evtType, callCallback, false); 
 
  event.initEvent(evtType, false, false); 
   
 
  fakeNode.dispatchEvent(event); 
   
  window.removeEventListener('error', handleWindowError); 
} 

当我们调用:

wrapperDev(() => {throw Error(123)}) 

会依次执行:

  • dispatchEvent触发事件回调callCallback
  • 在callCallback内执行到throw Error(123),抛出错误
  • callCallback执行中断,但调用他的函数会继续执行。
  • Error(123)被window error handler捕获用于Error Boundary

其中步骤2使Pause on exceptions不会失效。

步骤3、4使得错误被捕获,且不会阻止后续代码执行,模拟了try catch的效果。

总结

不得不说,React这波操作真细啊。

我们实现的迷你wrapper还有很多不足,比如:

  • 没有针对不同浏览器的兼容
  • 没有考虑其他代码也触发window error handler

React源码的完整版wrapper,见这里[3]

参考资料

[1]

GlobalEventHandlers.onerror MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/GlobalEventHandlers/onerror

[2]

EventTarget.dispatchEvent MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/dispatchEvent

[3]

这里: https://github.com/facebook/react/blob/master/packages/shared/invokeGuardedCallbackImpl.js#L63-L237


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK