摘要:進一步最終定位發現如果為的時候,效率驟降,如果為合法的字符串的時候,效率是正常值。每次執行該子句都會發生這種情況,將捕獲的異常對象分配給一個變量。盡可能將它們與其他代碼隔離,以免影響其性能。
關鍵代碼拆解成如下圖所示(無關部分已省略):
起初我認為可能是這個 getRowDataItemNumberFormat
函數里面某些方法執行太慢,從 formatData.replace
到 unescape
(已廢棄,官方建議使用 decodeURI
或者 decodeURIComponent
替代) 方法都懷疑了一遍,發現這些方法都不是該函數運行慢的原因。為了深究原因,我給 style.formatData
傳入了不同的值,發現這個函數的運行效率出現不同的表現。開始有點疑惑為什么 style.formatData
的值導致這個函數的運行效率差別如此之大。
進一步最終定位發現如果 style.formatData
為 undefined 的時候,效率驟降,如果 style.formatData
為合法的字符串的時候,效率是正常值。我開始意識到這個問題的原因在那里了,把目光轉向了 try catch
代碼塊,這是一個很可疑的地方,在很早之前曾經聽說過不合理的 try catch
是會影響性能的,但是之前從沒遇到過,結合了一些資料,我發現比較少案例去探究這類代碼片段的性能,我決定寫代碼去驗證下:
window.a = a;window.c = undefined;function getRowDataItemNumberFormatTryCatch() { console.time(getRowDataItemNumberFormatTryCatch); for (let i = 0; i < 3000; i++) { try { a.replace(/%022/g, "); } catch (error) { } } console.timeEnd(getRowDataItemNumberFormatTryCatch);}
我嘗試把 try catch
放入一個 for
循環中,讓它運行 3000 次,看看它的耗時為多少,我的電腦執行該代碼的時間大概是 0.2 ms 左右,這是一個比較快的值,但是這里 a.replace
是正常運行的,也就是 a
是一個字符串能正常運行 replace
方法,所以這里的耗時是正常的。我對他稍微做了一下改變,如下:
function getRowDataItemNumberFormatTryCatch2() { console.time(getRowDataItemNumberFormatTryCatch); for (let i = 0; i < 3000; i++) { try { c.replace(/%022/g, "); } catch (error) { } } console.timeEnd(getRowDataItemNumberFormatTryCatch);}
這段代碼跟上面代碼唯一的區別是,c.replace
此時應該是會報錯的,因為 c
是 undefined
,這個錯誤會被 try catch
捕捉到,而上面的代碼耗時出現了巨大的變化,上升到 40 ms,相差了將近 200 倍!并且上述代碼和首圖的 getRowDataItemNumberFormat 函數代碼均出現了 Minor GC
,注意這個 Minor GC
也是會耗時的。
這可以解釋一部分原因了,我們上面運行的代碼是一個性能比較關鍵的部分,不應該使用 try catch
結構,因為該結構是相當獨特的。與其他構造不同,它運行時會在當前作用域中創建一個新變量。每次 catch
執行該子句都會發生這種情況,將捕獲的異常對象分配給一個變量。
即使在同一作用域內,此變量也不存在于腳本的其他部分中。它在 catch
子句的開頭創建,然后在子句末尾銷毀。因為此變量是在運行時創建和銷毀的(這些都需要額外的耗時!),并且這是 JavaScript
語言的一種特殊情況,所以某些瀏覽器不能非常有效地處理它,并且在捕獲異常的情況下,將捕獲處理程序放在性能關鍵的循環中可能會導致性能問題,這是我們為什么上面會出現 Minor GC
并且會有嚴重耗時的原因。
如果可能,應在代碼中的較高級別上進行異常處理,在這種情況下,異常處理可能不會那么頻繁發生,或者可以通過首先檢查是否允許所需的操作來避免。上面的 getRowDataItemNumberFormatTryCatch2
函數示例顯示的循環,如果里面所需的屬性不存在,則該循環可能引發多個異常,為此性能更優的寫法應該如下:
function getRowDataItemNumberFormatIf() { console.time(getRowDataItemNumberFormatIf); for (let i = 0; i < 3000; i++) { if (c) { c.replace(/%022/g, "); } } console.timeEnd(getRowDataItemNumberFormatIf)}
上面的這段代碼語義上跟 try catch
其實是相似的,但運行效率迅速下降至 0.04ms,所以 try catch
應該通過檢查屬性或使用其他適當的單元測試來完全避免使用此構造,因為這些構造會極大地影響性能,因此應盡量減少使用它們。
如果一個函數被重復調用,或者一個循環被重復求值,那么最好避免其中包含這些構造。它們最適合僅執行一次或僅執行幾次且不在性能關鍵代碼內執行的代碼。盡可能將它們與其他代碼隔離,以免影響其性能。
例如,可以將它們放在頂級函數中,或者運行它們一次并存儲結果,這樣你以后就可以再次使用結果而不必重新運行代碼。
getRowDataItemNumberFormat
在經過上述思路改造后,運行效率得到了質的提升,在實測 300 多次循環中減少的時間如下圖,足足優化了將近 2s 多的時間,如果是 3000 次的循環,那么它的優化比例會更高:
由于上面的代碼是從項目中改造出來演示的,可能并不夠直觀,所以我重新寫了另外一個相似的例子,代碼如下,這里面的邏輯和上面的 getRowDataItemNumberFormat
函數講道理是一致的,但是我讓其發生錯誤的時候進入 catch
邏輯執行任務。
事實上 plus1
和 plus2
函數的代碼邏輯是一致的,只有代碼語義是不相同,一個是返回 1,另一個是錯誤拋出1,一個求和方法在 try
片段完成,另一個求和方法再 catch
完成,我們可以粘貼這段代碼在瀏覽器分別去掉不同的注釋觀察結果。
我們發現 try
片段中的代碼運行大約使用了 0.1 ms,而 catch
完成同一個求和邏輯卻執行了大約 6 ms,這符合我們上面代碼觀察的預期,如果把計算范圍繼續加大,那么這個差距將會更加明顯,實測如果計算 300000 次,那么將會由原來的 60 倍差距擴大到 500 倍,那就是說我們執行的 catch
次數越少折損效率越少,而如果我們執行的 catch
次數越多那么折損的效率也會越多。
所以在不得已的情況下使用 try catch
代碼塊,也要盡量保證少進入到 catch
控制流分支中。
const plus1 = () => 1;const plus2 = () => { throw 1 };console.time(sum);let sum = 0;for (let i = 0; i < 3000; i++) { try { // sum += plus1(); // 正確時候 約 0.1ms sum += plus2(); // 錯誤時候 約 6ms } catch (error) { sum += error; }}console.timeEnd(sum);
上面的種種表現進一步引發了我對項目性能的一些思考,我搜了下我們這個項目至少存在 800 多個 try catch
,糟糕的是我們無法保證所有的 try catch
是不損害代碼性能并且有意義的,這里面肯定會隱藏著很多上述類的 try catch
代碼塊。
從性能的角度來看,目前 V8
引擎確實在積極的通過 try catch
來優化這類代碼片段,在以前瀏覽器版本中上面整個循環即使發生在 try catch
代碼塊內,它的速度也會變慢,因為以前瀏覽器版本會默認禁用 try catch
內代碼的優化來方便我們調試異常。
而 try catch
需要遍歷某種結構來查找 catch
處理代碼,并且通常以某種方式分配異常(例如:需要檢查堆棧,查看堆信息,執行分支和回收堆棧)。盡管現在大部分瀏覽器已經優化了,我們也盡量要避免去寫出上面相似的代碼,比如以下代碼:
try { container.innerHTML = "Im alloyteam";}catch (error) { // todo}
上面這類代碼我個人更建議寫成如下形式,如果你實際上拋出并捕獲了一個異常,它可能會變慢,但是由于在大多數情況下上面的代碼是沒有異常的,因此整體結果會比異常更快。
這是因為代碼控制流中沒有分支會降低運行速度,換句話說就是這個代碼執行沒錯誤的時候,沒有在 catch 中浪費你的代碼執行時間,我們不應該編寫過多的 try catch
這會在我們維護和檢查代碼的時候提升不必要的成本,有可能分散并浪費我們的注意力。
當我們預感代碼片段有可能出錯,更應該是集中注意力去處理 success
和 error
的場景,而非使用 try catch
來保護我們的代碼,更多時候 try catch
反而會讓我們忽略了代碼存在的致命問題。
if (container) container.innerHTML = "Im alloyteam";else // todo
在簡單代碼中應當減少甚至不用 try catch
,我們可以優先考慮 if else
代替,在某些復雜不可測的代碼中也應該減少 try catch
(比如異步代碼),我們看過很多 async
和 await
的示例代碼都是結合 try catch
的,在很多性能場景下我認為它并不合理,個人覺得下面的寫法應該是更干凈,整潔和高效的。
因為 JavaScript
是事件驅動的,雖然一個錯誤不會停止整個腳本,但如果發生任何錯誤,它都會出錯,捕獲和處理該錯誤幾乎沒有任何好處,代碼主要部分中的 try catch
代碼塊是無法捕獲事件回調中發生的錯誤。
通常更合理的做法是在回調方法通過第一個參數傳遞錯誤信息,或者考慮使用 Promise
的 reject()
來進行處理,也可以參考 node
中的常見寫法如下:
;(async () => { const [err, data] = await readFile(); if (err) { // todo };})()fs.readFile(
結合了上面的一些分析,我自己做出一些淺顯的總結:
try catch
來捕獲異常。try catch
,確保異常路徑在需要考慮性能情況下優先考慮 if else
,不考慮性能情況請君隨意,而異步可以考慮回調函數返回 error
信息對其處理或者使用 Promse.reject()
。try catch
使用,也不要用它來保護我們的代碼,其可讀性和可維護性都不高,當你期望代碼是異常時候,不滿足上述1,2的情景時候可考慮使用。最后,筆者希望這篇文章能給到你我一些方向和啟發吧,如有疏漏不妥之處,還請不吝賜教!附筆記鏈接,閱讀往期更多優質文章可移步查看,喜歡的可以給我點贊鼓勵哦:https://github.com/Wscats/CV/issues/33
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/125675.html
摘要:由虛擬機生成并拋出,,屬于系統內部錯誤或者資源耗盡等嚴重情況,屬于需要擔負的責任,這一類異常事件是無法恢復或者不可能捕獲的,將導致應用程序中斷,但是自定義是可以捕獲的。 題目 showImg(http://img-storage.qiniudn.com/15-9-22/50608386.jpg); 答案:D 分析 Java 異常的結構體系 showImg(http://img-stor...
作者簡介 藍寅,開源分布式中間件DBLE項目負責人;持續專注于數據庫方面的技術, 始終在一線從事開發;對數據復制,讀寫分離,分庫分表的有深入的理解與實踐。 問題起因: 用benchmarksql_for_mysql對原生MyCat-1.6.1和DBLE-2.17.07版做性能測試對比,發現DBLE性能只到原生版MyCat的70%左右。 問題分析過程: 分析過程主要有以下內容:包括現象,收集數據,分...
摘要:下面我跟大家分享關于標識符查找方面的優化問題。這個變量對象會首先被放入作用域鏈中。執行上下文也有一個作用域鏈,這個作用域鏈就是用來進行變量查找的。當執行上下文創建時,它的作用域鏈會用函數的屬性來初始化。 前面兩篇文章介紹了Javascript文件在頁面中位置以及異步加載問題對前端性能的影響。不過受限于單線程的原因,不管采用哪種方法,只要Javascript進行了耗時的工作,就都會引...
閱讀 713·2023-04-25 19:43
閱讀 3910·2021-11-30 14:52
閱讀 3784·2021-11-30 14:52
閱讀 3852·2021-11-29 11:00
閱讀 3783·2021-11-29 11:00
閱讀 3869·2021-11-29 11:00
閱讀 3558·2021-11-29 11:00
閱讀 6105·2021-11-29 11:00