摘要:現狀最近在寫歡迎的時候,一直為錯誤的棧追蹤而愁。由于送入隊列的是函數,因此在的參數可以放心地使用。其次,這些函數并不是立即在中調用的,而是由專門的隊列處理代碼來調用。
本文的講述都是以 Node.js 環境為例子,而 Node.js 使用的 JavaScript 引擎是 V8,因此理論上 Chrome 也能適用,其它瀏覽器我就不清楚了。
現狀最近在寫 Rize(歡迎 star) 的時候,一直為錯誤的棧追蹤而愁。為什么呢?這要從 Rize 的架構說起。
由于 puppeteer 的絕大多數操作和 API 是異步的,而寫異步代碼的良好寫法是用 ES2017 的 async/await 語法。
但我們都知道,async/await 實際上返回的是一個 Promise(即使你沒有顯式地 return 什么,它將是 Promise
所以我使用了一個隊列來保存用戶想要進行的操作。也就是說,用戶在調用 Rize 的 API 之后,并不會(也不可能)立即執行這些操作,而是放在隊列中,等待時機適合(例如瀏覽器已經啟動或者上一個操作已經完成)才執行。由于送入隊列的是函數,因此在 push 的參數可以放心地使用 async/await。
但是,一旦這些操作中出現錯誤,錯誤的定位變得十分麻煩。
下面這張圖是直接用 Node.js 運行一個腳本的結果:
下面這張圖是在 Jest 中執行一段代碼的結果:
原因是,
首先,隊列中的函數是 async function,這本來就給 debug 帶來麻煩。
其次,這些函數并不是立即在 API 中調用的,而是由專門的隊列處理代碼來調用。在錯誤發生時,V8 只能跟蹤到那段隊列處理代碼那里。
這就為用戶帶來麻煩。錯誤發生了,卻只能看著錯誤消息一點一點地去試著定位有問題的地方。
探索為此我去閱讀了 Node.js 的官方文檔,看了 Errors 這一部分,不過似乎沒什么收獲。
后來又找到了 TJ Holowaychuk 大神寫的庫 callsite,看看能不能有用。從文檔上看,這個庫并不適合我的需求。
但我閱讀了 callsite 的源碼,源碼很短,十行不到。我在源碼發現了一些信息。
callsite 是利用 V8 的 Stack Trace API 來獲取函數調用處的一些信息,如文件名,行號等等。callsite 是如何獲取這些數據的呢?
非常簡單,就一句:
var err = new Error()
對,僅僅是 new 一個 Error 實例,而且并不是要拋出這個錯誤。
對比我們平時的代碼,通常當我們 throw 一個錯誤之后,我們能得到一些錯誤棧信息。但實際上,不需要 throw,僅僅是新建一個 Error 實例,也能讓 V8 記錄下當前的調用棧信息。
解決既然發現這個事實,那我們可以在需要記錄調用棧的地方 new 一個 Error 實例。(千萬不要把它拋出,不然你后面的代碼就沒法執行了)
此時當前的棧信息已經被記錄下來,那么我們怎樣去使用這些信息呢?
如果用戶的代碼執行正常,那就沒什么關系了。關鍵是在發生錯誤的時候。這里要提一提的是,我的那段隊列處理代碼是帶有 try…catch 塊的,大概長這樣:
try { await fn() } catch (error) { throw error } finally { // do some stuff ... }
你可能好奇什么要把捕捉的異常還要拋出,因為我想要的是后面的 finally 塊啊,但同時我又希望異常能繼續被拋出。
在這里,我們就要對 catch 塊做點功夫。當然這個 try…catch 塊是能夠獲取到之前新建的 Error 實例的,在這里我省略了那部分代碼。
為了方便敘述,我把之前 new 的那個 Error 實例命名為 trace,即假設 const trace = new Error()。
顯然把 trace 的所有棧信息都拿過來是不適合的,因為它有一些我們并不需要的棧信息(這部分信息是位于 API 調用處以上的)。
每一個 Error 實例都有個 stack 屬性,它是一個多行字符串,我們先把它的每行分開,保存在數組中:
const stack = trace.stack!.split(" ")
要注意 stack 的第一行不是棧信息,而是錯誤消息,這個不能去掉。所以:
stack.splice(1, 2)
我這里有兩行的信息是沒用的,所以刪去兩行,實際上要根據你的需要修改第二個參數。
現在可以把 trace 的棧信息替換掉實際 error 的棧信息:
error.stack = stack.join(" ")結果
現在就可以得到友好的錯誤棧信息了:
配合 Jest 就能更好地定位問題所在之處:
最后是宣傳一下我正在寫的庫 Rize(可以讓你簡單優雅地使用 puppeteer),也就是本文提到的,歡迎前往 GitHub 并 star。
博客原文在這里
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/93303.html
摘要:調用棧是一種單線程編程語言,這意味著它只有一個調用棧。這就是調用棧的功能。簡單代碼示例當引擎執行這段代碼時,調用棧為空,之后運行如下每個叫做堆棧幀。調用棧就是通過堆棧幀來追蹤異常,堆棧幀基本就是調用棧出現異常時候的狀態。 概述 幾乎每個人都已經聽說過V8引擎這個概念,而且大多人都知道JavaScript是單線程的,并且使用回調隊列。 這篇文章中,我們將詳細介紹這些概念,并解釋JavaS...
摘要:在運行腳本時,需要顯示的指定對象。大對象區每一個區域都是由一組內存頁構成的。這里是唯一擁有執行權限的內存區。換句話說,是該對象被之后所能回收到內存的總和。一旦活躍對象已被移出,則在舊的半空間中剩下的任何死亡對象被丟棄。 內存管理 本文以V8為背景 對之前的文章進行重新編輯,內容做了很多的調整,使其具有邏輯更加緊湊,內容更加全面。 1. 基礎概念 1.1 生命周期 不管什么程序語言,內存...
摘要:是如何工作的引擎,運行時以及調用棧的概述原文譯者隨著變得越來越流行,團隊在多個層級都對它進行利用前端,后端,混合應用,嵌入式設備以及更多。這個將會在是如何工作的的第二部分進一步解釋。 How JavaScript works: an overview of the engine, the runtime, and the call stack JavaScript是如何工作的:引擎,運...
摘要:調用棧是一種數據結構,它記錄了我們在程序中的位置。當從這個函數返回的時候,就會將這個函數從棧頂彈出,這就是調用棧做的事情。而且這不是唯一的問題,一旦你的瀏覽器開始處理調用棧中的眾多任務,它可能會停止響應相當長一段時間。 原文地址: https://blog.sessionstack.com... PS: 好久沒寫東西了,最近一直在準備寫一個自己的博客,最后一些技術方向已經敲定了,又可以...
摘要:本章會對語言引擎,運行時,調用棧做一個概述。調用棧只是一個單線程的編程語言,這意味著它只有一個調用棧。查看如下代碼當引擎開始執行這段代碼的時候,調用棧會被清空。之后,產生如下步驟調用棧中的每個入口被稱為堆棧結構。 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原...
閱讀 2850·2021-08-20 09:37
閱讀 1607·2019-08-30 12:47
閱讀 1090·2019-08-29 13:27
閱讀 1685·2019-08-28 18:02
閱讀 749·2019-08-23 18:15
閱讀 3084·2019-08-23 16:51
閱讀 931·2019-08-23 14:13
閱讀 2125·2019-08-23 13:05