不用try catch,如何机智的捕获错误
友情提醒:文末抽奖送4本《JavaScript高级程序设计》第四版
这是多个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
面板的一个功能。
开启该功能后,在运行时遇到会抛出错误的代码,代码的执行会自动停在该行,就像在该行打了断点一样。
比如,执行如下代码,并开启该功能:
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
:
// 开发环境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
参考资料
[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
学习交流
- 关注公众号【前端宇宙】,每日获取好文推荐
- 添加微信,入群交流
“在看和转发”就是最大的支持
- Is It A Tree?(并查集)
- Constructing Roads(最小生成树)
- More is better(并查集)
- 快来给这个图表起个名字吧~
- Prim
- 仿经济学人——矩阵气泡图
- 左手用R右手Python系列——模拟登陆教务系统
- R语言数据清洗实战——复杂数据结构与list解析
- R语言爬虫实战——知乎live课程数据爬取实战
- Python爬虫系列(二)Quotes to Scrape(谚语网站的爬取实战)
- R语言数据清洗实战——世界濒危遗产地数据爬取案例
- Leetcode-Easy 437. Path Sum III
- R语言爬虫实战——网易云课堂数据分析课程板块数据爬取
- 左手用R右手Python系列14——日期与时间处理
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 学习使用Material Design控件(四)Android实现标题栏自动缩放、放大效果
- Ubuntu 20.04 CUDA&cuDNN安装方法(图文教程)
- Android开发之基于DialogFragment创建对话框的方法示例
- Android图片压缩的实例详解
- Android编程之手机壁纸WallPaper设置方法示例
- 手把手教你在腾讯云上搭建hadoop3.x伪集群的方法
- Android从网络中获得一张图片并显示在屏幕上的实例详解
- Android ListView中headerview的动态显示和隐藏的实现方法
- Android编程使用Service实现Notification定时发送功能示例
- Android基于ViewFilpper实现文字LED显示效果示例
- Android ViewPager导航小圆点实现无限循环效果
- ViewPager打造轮播图Banner/引导页Guide
- Android 实现带字母索引的侧边栏功能
- Android实现简单底部导航栏 Android仿微信滑动切换效果
- Android中Handler与Message的简单实例