摘要:無論你使用的是解釋型語言還是編譯型語言,都有一個共同的部分將源代碼作為純文本解析為抽象語法樹的數據結構。和抽象語法樹相對的是具體語法樹,通常稱作分析樹。這是引入字節碼緩存的原因。
這是專門探索 JavaScript 及其所構建的組件的系列文章的第 14 篇。
想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你!
如果你錯過了前面的章節,可以在這里找到它們:
JavaScript 是如何工作的:引擎,運行時和調用堆棧的概述!
JavaScript 是如何工作的:深入V8引擎&編寫優化代碼的5個技巧!
JavaScript 是如何工作的:內存管理+如何處理4個常見的內存泄漏 !
JavaScript 是如何工作的:事件循環和異步編程的崛起+ 5種使用 async/await 更好地編碼方式!
JavaScript 是如何工作的:深入探索 websocket 和HTTP/2與SSE +如何選擇正確的路徑!
JavaScript 是如何工作的:與 WebAssembly比較 及其使用場景 !
JavaScript 是如何工作的:Web Workers的構建塊+ 5個使用他們的場景!
JavaScript 是如何工作的:Service Worker 的生命周期及使用場景!
JavaScript 是如何工作的:Web 推送通知的機制!
JavaScript是如何工作的:使用 MutationObserver 跟蹤 DOM 的變化!
JavaScript是如何工作的:渲染引擎和優化其性能的技巧!
JavaScript是如何工作的:深入網絡層 + 如何優化性能和安全!
JavaScript是如何工作的:CSS 和 JS 動畫底層原理及如何優化它們的性能!
概述我們都知道運行一大段 JavaScript 代碼性能會變得很糟糕。這段代碼不僅需要通過網絡傳輸,而且還需要解析、編譯成字節碼,最后執行。在之前的文章中,我們討論了 JS 引擎、運行時和調用堆棧等,以及主要由谷歌 Chrome 和 NodeJS 使用的V8引擎。它們在整個 JavaScript 執行過程中都發揮著至關重要的作用。這篇說的抽象語法樹同樣重要:在這我們將了解大多數 JavaScript 引擎如何將文本解析為對機器有意義的內容,轉換之后發生的事情以及做為 Web 開發者如何利用這一知識。
編程語言原理那么,首先讓我們回顧一下編程語言原理。不管你使用什么編程語言,你需要一些軟件來處理源代碼以便讓計算機能夠理解。該軟件可以是解釋器,也可以是編譯器。無論你使用的是解釋型語言(JavaScript、Python、Ruby)還是編譯型語言(c#、Java、Rust),都有一個共同的部分:將源代碼作為純文本解析為 抽象語法樹(abstract syntax tree, AST) 的數據結構。
AST 不僅以結構化的方式顯示源代碼,而且在語義分析中扮演著重要角色。在語義分析中,編譯器驗證程序和語言元素的語法使用是否正確。之后,使用 AST 來生成實際的字節碼或者機器碼。
抽象語法樹(abstract syntax tree 或者縮寫為 AST),或者語法樹(syntax tree),是源代碼的抽象語法結構的樹狀表現形式,這里特指編程語言的源代碼。和抽象語法樹相對的是具體語法樹(concrete syntaxtree),通常稱作分析樹(parse tree)。一般的,在源代碼的翻譯和編譯過程中,語法分析器創建出分析樹。一旦 AST 被創建出來,在后續的處理過程中,比如語義分析階段,會添加一些信息。AST 程序
AST 不僅僅是用于語言解釋器和編譯器,在計算機世界中,它們還有多種應用。使用它們最常見的方法之一是進行靜態代碼分析。靜態分析器不執行輸入的代碼,但是,他們仍然需要理解代碼的結構。
例如,你可能想要實現一個工具,該工具可以找到公共代碼結構,以便你可以重構它們以減少重復。你可能會通過使用字符串比較來實現這一點,但這個會相當簡單且有局限性。
當然,如果你對實現這樣的工具感興趣,你不需要編寫自己的解析器。有許多與 Ecmascript規范完全兼容的開源項目。Esprima 和 Acorn 即是黃金搭檔,還有許多工具可以幫助解析器生成輸出,即 ASTs ,ASTs 被廣泛應用于代碼轉換。
例如,你可能希望實現一個將 Python 代碼轉換為J avaScript 的轉換器。基本思想是使用Python 轉換器生成 AST,然后使用 AST 生成JavaScript代碼。
你可能會覺得難以置信,事實是 ASTs 只是部分語言的不同表示法。在解析之前,它被表示為遵循一些規則的文本,這些規則構成了一種語言。在解析之后,它被表示為一個樹結構,其中包含與輸入文本完全相同的信息。因此,也可以進行反向解析然后回到文本。
JavaScript 解析讓我們看看 AST 是如何構建的。我們用一個簡單的 JavaScript 函數作為例子:
function foo(x) { if (x > 10) { var a = 2; return a * x; } return x + 10; }
解析器會產生如下的 AST:
注意,為了觀看方便,這里是解析器將生成的結果的簡化版本。實際的 AST 要復雜得多。然而,這里的目的是為了運行源碼之前的第一個步驟前。如果人想查看實際的 AST 是什么樣子,可以訪問 AST Explorer。它是一個在線工具,你以在其中輸入一些 JavaScript 并輸出對應的 AST。
你可能會問,為什么需要知道 JavaScript解析器工作原理,畢竟這是瀏覽器工作,你想法是部分正確。下圖展示了 JavaScript 執行過程中不同階段的耗時。仔細瞅瞅,你或許會發現一些有趣的東西。
發現沒? 通常情況下,瀏覽器解析 JavaScript 大約需占總執行時間的 15% 到 20%。我沒有具體統計過這些數值。這些是來自真實應用程序和以某種方式使用 JavaScript 的網站的統計數據。也許 15% 看起來不是很多,但相信我,這是很多。
一個典型的單頁程序加載 0.4 mb 左右的 JavaScript,瀏覽器需要大約 370ms 來解析它。也許你會又說,這也不是很多嘛,本身花費的時間并不多。但請記住,這只是將 JavaScript 代碼解析為 AST 所需要的時間。這并不包括運行本身的時間,也不包括在頁面加載 ,如 CSS 和 HTML 渲染過程的耗時。這些還只涉及桌面,移動瀏覽器的情況會更加復雜,在手機上花在解析上的時間通常是桌面瀏覽器的 2 到 5 倍。
上圖顯示了 1MB JavaScript 包在不同類的移動和桌面瀏覽器解析時間。
更重要的是,為了獲得更多類原生的用戶體驗而把越來越多的業務邏輯堆積在前端,Web 應用程序正變得越來越復雜。你可以輕易地想到網絡應用受到的性能影響。只需打開瀏覽器開發工具,然后使用該工具來解析、編譯和瀏覽器中發生的所有其他事情上所消耗的時間。
不幸的是,移動瀏覽器上沒有開發者工具。不過不用擔心,這并不意味著你對此無能為力。因為有 DeviceTiming 工具,它可以用來幫助檢測受控環境中腳本的解析和運行時間。它通過插入代碼來封裝本地代碼,這樣每次從不同的設備訪問頁面時,就可以在本地測量解析和運行時間。
好事就是 JavaScript 引擎做了很多工作來避免冗余的工作,并得到了更好的優化,以下為主流瀏覽器使用的技術。
例如,V8 實現腳本流(script streaming)和代碼緩存技術。腳本流即腳本一旦開始下載,async 和 deferred的 腳本就會在多帶帶的線程上解析。這意味著在下載腳本完成后幾乎立即完成解析,這會提升 10% 的頁面加載速度。
每次訪問頁面時,JavaScript 代碼通常編譯為字節碼。 然而,一旦用戶訪問另一頁面,該字節碼就被丟棄。 發生這種情況是因為編譯后的代碼很大程度上依賴于編譯時機器的狀態和上下文。 這是 Chrome 42 引入字節碼緩存的原因。 該技術會本地緩存編譯過的代碼,這樣當用戶返回同一頁面時,諸如下載,解析和編譯等所有步驟都會被跳過。 這使得 Chrome 可以節省大約 40% 的解析和編譯時間。 此外,這還可以節省移動設備的電量。
在 Opera 中,Carakan 引擎可以重用另一個程序最近編譯過的輸出。沒有要求代碼必須來自相同的頁面甚至同個域下。這種緩存技術實際上非常高效,還可以完全跳過編譯步驟。它依賴于典型的用戶行為和瀏覽場景:每當用戶在應用程序/網站中遵循某個用戶的特定瀏覽習慣,都會加載相同的 JavaScript 代碼。不過,Carakan 引擎早已被谷歌的 V8 所取代。
Opera 新的 JavaScript 引擎 “Carakan”,目前速度是其他已存在 JavaScript 引擎(基于 SunSpider)的2.5倍。其在轉化為本地機器代碼時專門針對正則表達式做了優化。
Firefox 使用的 SpiderMonkey 引擎不會緩存所有內容。它可以過渡到監視階段,在這個階段中,它計算執行給定腳本的次數。基于此計算,它推導出頻繁使用而可以被優化的代碼部分。
SpiderMonkey 是 Mozilla 項目的一部分,是一個用 C 語言實現的 JavaScript 腳本引擎,另外還有一個叫做Rhino 的 Java 版本。
顯然,有些人決定什么都不做。Safari 的首席開發人員 Maciej Stachowiak 表示,Safari 不會對編譯后的字節碼進行任何緩存。緩存技術他們是有考慮過的問題,但是他們還沒有實現,因為生成代碼的耗時小于總運行時間的 2%。
這些優化不會直接影響 JavaScript 源代碼的解析,但是會盡可能完全避免。畢竟做總比沒做好點?
我們可以做很多事情來改善應用程序的初始加載時間。最小化加載的 JavaScript 數量:代碼越小、解析所需要時間就越少,運行時間也就越小。要做到這一點,我們只能在當前的路由上加載所需的代碼,而不是加載一大陀的代碼。例如,PRPL模式即表示該種代碼傳輸類型。或者,可以檢查代碼的依賴關系,看看是否有什么冗余的依賴導致代碼庫膨脹,然而,這些東西需要很大的篇幅來進行討論。
本文的主要的目的討論作為 Web 開發人員可以做些什么來幫助 JavaScript 解析器更快地完成它的工作。還有,現代JavaScript 解析器使用 啟發法(heuristics) 來決定是否立即運行指定的代碼片段或者推遲在未來的某個時候運行。基于這些啟發法,解析器將進行即時或懶解析。
啟發法是針對模型求解方法而言的,是一種逐次逼近最優解的方法。這種方法對所求得的解進行反復判斷實踐修正直至滿意為止。啟發法的特點是模型簡單,需要進行方案組合的個數少,因此便于找出最終答案。此方法雖不能保證得到最優解,但只要處理得當,可獲得決策者滿意的近似最優解。一般步驟包括:定義一個計算總費用的方法;報定判別準則;規定方案改選的途徑;建立相應的模型;送代求解。
立即解析會運行需要立即編譯的函數。它主要做三件事:構建 AST,構建作用域層級和查找所有語法錯誤。另一方面, 懶解析只運行未編譯的函數。它不構建AST,也不查找所有語法錯誤,它只構建作用域層級,與立即解析相比節省了大約一半的時間。
顯然,這不是一個新概念。即使像 IE 9 這樣的瀏覽器也支持這種類型的優化,盡管與現在的解析器的工作方式相比,這種優化方式還很初級。
來看一個例子,假設有以下代碼片段:
function foo() { function bar(x) { return x + 10; } function baz(x, y) { return x + y; } console.log(baz(100, 200)); } foo()
就像前面的例子一樣,代碼被輸入到語法分析器中,語法分析器進行語法分析并輸出AST,如下:
聲明函數 foo
調用函數 foo
在 foo 里聲明函數 bar 接收參數 x, 并返回 x 和 10 相加的結果
在 foo 里聲明函數 baz 接收參數 x和 y, 并返回 x 和 y 相加的結果
調用 baz 函數傳入 100 和 2。
調用 console.log 參數為之前函數調用的返回值。
那么期間發生了什么? 解析器看到 bar 函數的聲明、baz 函數的聲明、bar函數的調用和 console.log 的調用。但是,解析器做了一些完全無關的額外工作即解析 bar 函數。為什么這無關緊要? 因為函數 bar 從來沒有被調用過(或者至少在那個時候沒有)。這是一個簡單的示例,看起來可能有些不同尋常,但在許多實際應用程序中,許多聲明的函數從未被調用。
這里不解析bar函數,該函數聲明了卻沒有調用它。只在需要的時候在函數運行前進行真正的解析。懶解析仍然需要找到函數的整個主體并為其聲明,但僅此而已。它不需要語法樹,因為它還沒有被處理。另外,它不會從堆中分配內存,而堆通常會占用相當多的系統資源,簡而言之,跳過這些步驟會帶來很大的性能改進。
所以之前的例子,解析器實際上會像如下這樣解析:
注意,這里只確認 bar 函數聲明,沒有進入 bar 函數體。在這種情況下,函數體只是一個返回語句。但是,與大多數實際應用程序一樣,它可以更大,包含多個返回語句、條件語句、循環、變量聲明,甚至嵌套函數聲明。這完全是在浪費時間和系統資源,因為這個函數永遠不會被調用。
這是一個相當簡單的概念,但實際上,它的實現是非常難的,不局限于以上示例。整個方法還可以適用于函數、循環、條件、對象等。基本上,所有需要解析的東西。
例如,下面是一個非常常見的 JavaScript 模式。
var myModule = (function() { // 整個模塊的邏輯 // 返回模塊對象 })();
大多數現代 JavaScript 解析器都能識別這種模式,此模式表示代碼需要立即解析。
那么為什么解析器不都使用懶解析呢? 如果懶解析某些代碼,這些代碼需要立即執行,這實際上會使代碼運行速度變慢。需要運行一次懶解析之后進行另一個立即解析,這和立即解析相比,運行速度會慢 50%。
現在對解析器底層原理有了大致的了解,是時候考慮如何提高解析器的解析速度。可以用這種方式編寫代碼,以便在正確的時間解析函數。大多數解析器都能識別一種模式:使用括號封裝函數。對于解析器來說,這幾乎總是一個積極的信號,即函數需要立即執行。如果解析器看到一個左括號,緊接著是一個函數聲明,它將立即解析這個函數。可以通過顯式地聲明立即執行的函數來幫助解析器加快解析速度。
假設有一個名為 foo 的函數。
function foo(x) { return x * 10; }
因為沒有明顯地標識表明需要立即運行該函數所以瀏覽器會進行懶解析。然而,我們確定這是不對的,那么可以運行兩個步驟。
首先,將函數存儲在一個變量中:
var foo = function foo(x) { return x * 10; };
注意,這里有使用函數的名稱 foo,這不是必需的,但是建議這樣做,因為在拋出異常的情況下,stacktrace 會保留實際函數名稱,而不僅僅是
以上事例解析器執行懶解析,可以用括號封裝起來,讓解析器進行立即解析:
var foo = (function foo(x) { return x * 10; });
現在,解析器看見 function 關鍵字前的左括號便會立即進行解析。
因為需要知道解析器在哪些情況下執行懶解析或者立即解析,所以很難手動管理。此外,還需要花時間考慮是否立即調用某個函數,肯定沒人想這么做的。
最后,這種地讓代碼更難閱讀和理解。可以使用 Optimize.js 可以幫我們做這類事情,該工具只是用來優化 JavaScript 源代碼的初始加載時間,它們對代碼進行靜態分析,然后通過使用括號封裝需要立即運行的函數以便瀏覽器立即解析并準備運行它們。
像往常一樣編碼,然后有一段代碼看起來像這樣的:
(function() { console.log("Hello, World!"); })();
一切看起來都很好,如預期的那樣工作,而且速度很快,因為在函數聲明之前添加左括號。當然,在進入生產環境之前需要進行代碼壓縮,以下為壓縮工具的輸出:
!function(){console.log("Hello, World!")}();
好像沒問題,代碼像以前一樣工作。但是好像少了什么,壓縮工具刪除包裹函數的括號,而是在函數前放置了一個感嘆號,這意味著解析器將跳過此并將執行惰解析。
最重要的是,為了能夠執行該函數,它將在懶解析之后立即進行立即解析。 這會使代碼運行得更慢,幸運的是,可以利用 Optimize.js 來解決此類問題,傳給 Optimize.js 壓縮過的代碼會輸出如下代碼:
!(function(){console.log("Hello, World!")})();
這還差不多,現在擁有兩全其美方案:壓縮代碼且解析器正確地識別懶解析和立即解析的函數。
預編譯但為什么不能在服務器端完成所有這些工作呢? 畢竟,最好這樣做一次并將結果提供給客戶端,而不強制各個客戶端重復做該項事情。那么,目前正在討論引擎是否應該提供一種執行預編譯腳本的方法,這樣就可以節省瀏覽器運行時間。
從本質上講,該思路是擁有可以生成字節碼的務器端工具,這樣只需要傳輸字節碼并在客戶端運行,之后會看到啟動時間的一些主要差異。 這可能聽起來很誘人,但事情并非那么簡單,還可能會產生相反的效果,因為它會更大,并且很可能需要簽署代碼并出于安全原因對其進行處理。 例如,V8 團隊正在努力解決重復解析問題,這樣預編譯有可能實際并沒有多大的用處。
提升編譯速度一些建議檢查依賴,減少不必要的依賴
分割代碼為更小的塊而不是一整陀的
盡可能推遲加載 JavaScript,按需要加載或者動態加載。
使用開發者工具和 DeviceTiming 來檢測性能瓶頸
用像 Optimize.js 的工具來幫助解析器選擇立即解析或者懶解析以加快解析速度
原文:
https://blog.sessionstack.com...
代碼部署后可能存在的BUG沒法實時知道,事后為了解決這些BUG,花了大量的時間進行log 調試,這邊順便給大家推薦一個好用的BUG監控工具 Fundebug。
你的點贊是我持續分享好東西的動力,歡迎點贊!
交流干貨系列文章匯總如下,覺得不錯點個Star,歡迎 加群 互相學習。
https://github.com/qq44924588...
我是小智,公眾號「大遷世界」作者,對前端技術保持學習愛好者。我會經常分享自己所學所看的干貨,在進階的路上,共勉!
關注公眾號,后臺回復福利,即可看到福利,你懂的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/101269.html
摘要:事實是只是部分語言的不同表示法。基于這些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關的額外無用功即解析函數。這里不解析函數,該函數聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...
摘要:事實是只是部分語言的不同表示法。基于這些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關的額外無用功即解析函數。這里不解析函數,該函數聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...
摘要:事實是只是部分語言的不同表示法。基于這些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關的額外無用功即解析函數。這里不解析函數,該函數聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...
摘要:下面是用實現轉成抽象語法樹如下還支持繼承以下是轉換結果最終的結果還是代碼,其中包含庫中的一些函數。可以使用新的易于使用的類定義,但是它仍然會創建構造函數和分配原型。 這是專門探索 JavaScript 及其所構建的組件的系列文章的第 15 篇。 想閱讀更多優質文章請猛戳GitHub博客,一年百來篇優質文章等著你! 如果你錯過了前面的章節,可以在這里找到它們: JavaScript 是...
閱讀 2064·2023-04-25 22:58
閱讀 1408·2021-09-22 15:20
閱讀 2694·2019-08-30 15:56
閱讀 1986·2019-08-30 15:54
閱讀 2101·2019-08-29 12:31
閱讀 2728·2019-08-26 13:37
閱讀 592·2019-08-26 13:25
閱讀 2098·2019-08-26 11:58