不用try catch,如何機智的捕獲錯誤

2021-02-08 前端大全

(給前端大全加星標,提升前端技能)

作者: 魔術師卡頌 公號 / 卡頌 (本文來自作者投稿)

這是多個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







覺得本文對你有幫助?請分享給更多人

關注「前端大全」加星標,提升前端技能

好文章,我在看❤️

相關焦點

  • 為什麼推薦使用try-with-resources代替try-finally
    比如finalizer、try-catch-finally、try-with-resources等等。1、finally不是必要條件也就是說try-catch-finally中,可以只有try-catch,也可以只有try-finally。
  • 面試官:當return遇到try、catch、finally時會發生什麼?
    2.try,catch,finally的作用在Java中有檢查異常和非檢查異常(運行時異常)兩種異常:運行時異常,編譯時不被檢查的異常,不需要強制捕獲,編譯也能通過,他們是RuntimeException的子類。
  • 領導說:try-catch要放在循環體外!
    很多人對 try-catch 有一定的誤解,比如我們經常會把它(try-catch)和「低性能」直接畫上等號,但對 try-catch 的本質(是什麼)卻缺少著最基礎的了解,因此我們也會在本篇中對 try-catch 的本質進行相關的探索。
  • 「屎」一樣的try{}catch代碼,竟然可以這麼幹掉~
    但是繁瑣的try{}caht嵌套在代碼裡,看著很不舒服,這裡我們不討論性能,就代碼來講,來看看如何將他隱藏起來。原理是不變的。變得是寫法。下面我們來看如何優雅的處理異常塊。在這之前。你需要知道以下幾個概念:
  • 英語catch的短語,你理解多少?快來看看
    關於catch的短語,你知道多少?1.catch on理解;察覺變得流行滑板運動很快就流行起來Skateboarding caught on quickly.2.catch out偵測錯誤:偵測(另一人)正在犯錯的行為或過程3.catch up搶奪匪徒搶了錢夾並逃走了The mugger caught the wallet up and fled.
  • catch you later不是一會抓住你!
    「OK, catch you later!」 好的,再見!catch a cold 感冒To get sick with a cold (a minor respiratory infection).「Why weren’t you at the soccer game on Saturday?」
  • 英語高頻詞之catch
    你還只把catch理解為"抓住"嗎?可不止哦!catch不僅僅可以做動詞還作名詞。一起來看catch穿梭在不同場景中的用法!1,catch caught caught作動詞v. 抓住, 逮住,捕捉We caught a rabbit.我們抓了一隻兔子。
  • catch的用法總結,人贓並獲,英語怎麼說?catch sb red-handed
    今天我們來學習catch的用法。請熟讀下面生活中常見情景例句到會說。逮住,捕獲,發現趕上(公共汽車、火車、飛機等)得病;染疾聽清楚;領會The goalkeeper He leapt up and caught the ball.
  • 熟詞生義:「catch one's eye」不是指「抓住眼睛」!
    大家好,今天我們分享的一個表達——catch one's eye, 它的含義不是指「抓住眼睛」,其正確的含義是指:catch one's eye 引起…的注意,吸引…的目光 A sudden movement caught my eye.
  • 和catch有關的五個常見表達 5 Common Expressions with CATCH
    好的,大家可能已經聽說過「catch」這個動詞了吧?Okay so you've probably heard of this verb 'catch' right? But did you know that you can catch a cold?但是你們知道你們會感冒嗎?你們可能點火嗎?
  • python編程從入門到實踐:使用try-except代碼塊
    在用戶輸入的任何一個值不是數字時都捕獲ValueError異常,並列印一條友好的錯誤信息。對你編寫的程序進行測試:先輸入兩個數字,再輸入一些文本而不是數字。try:number1=int(input("請輸入一個數字:"))number2=int(input("請輸入一個數字:"))print(number1+number2)except ValueError
  • ...wait a few minutes and try again如何解決 出現英文代碼解決...
    導 讀 lol手遊we『ve received your request.please wait a few minutes and try again這個英文代碼
  • lol手遊an error occurred please try toagain later是什麼意思...
    LOL手遊an error occurred please try toagain later怎麼解決?這一串的英文提示一般出現在伺服器炸服的情況哦,具體如何解決an error occurred please try toagain later呢,現在就來告訴給大家詳細解答內容~
  • 聽歌學英文| Try
    , try, try, try你不必去試著努力You don't have to try, try, try, try你不必去試著努力You don't have to try, try, try, try