抛出错误_不用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面板的一個功能。
開啟該功能后,在運行時遇到會拋出錯誤的代碼,代碼的執行會自動停在該行,就像在該行打了斷點一樣。
比如,執行如下代碼,并開啟該功能:
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)})會依次執行:
其中步驟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
總結
以上是生活随笔為你收集整理的抛出错误_不用try catch,如何机智的捕获错误的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: linux下boost库链接动态库失败
- 下一篇: emplace_back和push_ba