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

开发
这是多个feature组合使用后实现的神奇效果,在React源码中被广泛使用。当我读源码看到这里时,心情经历了:懵逼 -- 困惑 -- 沉思 -- 查文档 -- 豁然开朗,看完此文,相信你也会发出感叹:还能这么玩?

[[344042]]

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

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

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

类似如下: 

  1. function wrapper(func) { 
  2.   try { 
  3.     func(); 
  4.   } catch(e) { 
  5.     // ...处理错误 
  6.   } 

比如触发componentDidMount时:

  1. wrapper(componentDidMount); 

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

这不,有人提issue:

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

Pause on exceptions失效的来龙去脉
Pause on exceptions是什么?

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

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

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

  1. let a = c; 

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

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

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

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

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

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

如何解决
对用户来说,我写在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:

  1. // 开发环境wrapper 
  2. function wrapperDev(func) { 
  3.   function handleWindowError(error) { 
  4.     // 收集错误交给Error Boundary处理 
  5.   } 
  6.  
  7.   window.addEventListener('error', handleWindowError); 
  8.   func(); 
  9.   window.removeEventListener('error', handleWindowError); 

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

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

  1. function wrapperPrd(func) { 
  2.   try { 
  3.     func(); 
  4.   } catch(e) { 
  5.     // ...处理错误 
  6.   } 

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

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

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

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

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

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

如何让代码执行不中断
答案是:通过dispatchEvent触发事件回调,在回调中调用用户代码。

根据EventTarget.dispatchEvent MDN[2]:

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

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

让我们继续改造wrapperDev。

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

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

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

  1. function callCallback() { 
  2.   fakeNode.removeEventListener(evtType, callCallback, false);  
  3.   func(); 
  4.  
  5. // 监听虚构的事件类型 
  6. fakeNode.addEventListener(evtType, callCallback, false); 
  7.  
  8. // 初始化事件 
  9. event.initEvent(evtType, falsefalse); 
  10.  
  11. // 触发事件 
  12. fakeNode.dispatchEvent(event); 

完整流程如下:

  1. function wrapperDev(func) { 
  2.   function handleWindowError(error) { 
  3.     // 收集错误交给Error Boundary处理 
  4.   } 
  5.    
  6.   function callCallback() { 
  7.     fakeNode.removeEventListener(evtType, callCallback, false);  
  8.     func(); 
  9.   } 
  10.    
  11.   const event = document.createEvent('Event'); 
  12.   const fakeNode = document.createElement('fake'); 
  13.   const evtType = 'fake-event'
  14.  
  15.   window.addEventListener('error', handleWindowError); 
  16.   fakeNode.addEventListener(evtType, callCallback, false); 
  17.  
  18.   event.initEvent(evtType, falsefalse); 
  19.    
  20.  
  21.   fakeNode.dispatchEvent(event); 
  22.    
  23.   window.removeEventListener('error', handleWindowError); 

当我们调用:

  1. 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

 

 

责任编辑:姜华 来源: 魔术师卡颂
相关推荐

2021-01-05 07:54:55

事项trycatch

2017-11-02 15:26:10

JavaScriptasync错误

2023-09-07 07:53:21

JavaScriptGoRust

2020-08-24 13:35:59

trycatchJava

2023-11-13 17:01:26

C++编程

2009-12-02 19:56:33

PHP中try{}ca

2021-03-31 11:52:24

try-catch-fJava代码

2022-06-16 10:37:09

asyncawait

2020-06-15 08:12:51

try catch代码处理器

2023-05-16 15:32:45

JavaScriptWeb前端工程师

2010-12-21 14:08:50

PowerShell

2009-07-21 14:30:38

Scalatry-catch

2019-07-15 07:58:10

前端开发技术

2021-01-13 09:55:29

try-catch-fJava代码

2021-08-03 07:51:43

React项目面试

2020-05-29 08:14:49

代码Try-Catch程序员

2020-06-28 09:08:08

Java语法块开发

2020-10-14 12:10:22

Javatry-catch代码

2013-03-13 11:28:13

测试捕获错误性能测试

2009-12-03 10:49:32

PHP自定义异常处理器
点赞
收藏

51CTO技术栈公众号