trycatch语法_不用try catch,如何机智的捕获错误

(给前端大全加星标,提升前端技能)

作者: 魔术师卡颂 公号 / 卡颂 (本文来自作者投稿)

这是多个feature组合使用后实现的神奇效果,在React源码中被广泛使用。

当我读源码看到这里时,心情经历了:

懵逼 -- 困惑 -- 沉思 -- 查文档 -- 豁然开朗

看完此文,相信你也会发出感叹:

还能这么玩?

起源

我们知道,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面板的一个功能。

11e56efc8a50733823b6f6364e6e06c6.png

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

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

let a = c;

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

f9a1b13b9766d41db985c3047eb67311.png

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

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

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

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

35a335023869865b5d9c6efd6a4eb992.png

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

如何解决

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

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

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

  • 捕获用户代码抛出的错误,使Error Boundary功能正常运行

  • 不捕获用户代码抛出的错误,使Pause on exceptions不失效

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

如何“捕获”错误

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

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

解决办法是:监听windowerror事件。

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

  • js运行时错误(包括语法错误)。window会触发ErrorEvent接口的error事件

  • 资源(如)加载失败错误。加载资源的元素会触发Event接口的error事件,可以在window上捕获该错误

实现开发环境使用的wrapperDev

// 开发环境wrapper
function wrapperDev(func) {
  function handleWindowError(error) {
    // 收集错误交给Error Boundary处理
  }

  window.addEventListener('error', handleWindowError);
  func();
  window.removeEventListener('error', handleWindowError);
}

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

但是,对比生产环境wrapperPrdfunc抛出的错误会被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)})

会依次执行:

  1. dispatchEvent触发事件回调callCallback

  2. callCallback内执行到throw Error(123),抛出错误

  3. callCallback执行中断,但调用他的函数会继续执行。

  4. Error(123)window error handler捕获用于Error Boundary

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

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

总结

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

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

  • 没有针对不同浏览器的兼容

  • 没有考虑其他代码也触发window error handler

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

关注魔术师卡颂,了解更多React源码相关知识。

参考资料

[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

937e1ac0ee93364cec3157792b5ab905.png

推荐阅读   点击标题可跳转

1、一位摸金校尉决定转行前端

2、唐朝人更懂 React

3、Vue 项目中各种痛点问题及方案

觉得本文对你有帮助?请分享给更多人

关注「前端大全」加星标,提升前端技能

a9c6db3c3457d61cf4c992e81eb22b64.png

好文章,我在看❤️

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值