(給前端大全加星標,提升前端技能)
作者: 魔術師卡頌 公號 / 卡頌 (本文來自作者投稿)
這是多個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
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
覺得本文對你有幫助?請分享給更多人
關注「前端大全」加星標,提升前端技能
好文章,我在看❤️