国产xxxx99真实实拍_久久不雅视频_高清韩国a级特黄毛片_嗯老师别我我受不了了小说

資訊專欄INFORMATION COLUMN

JavaScript 工作原理之十四-解析,語法抽象樹及最小化解析時間的 5 條小技巧

jsliang / 1625人閱讀

摘要:事實是只是部分語言的不同表示法。基于這些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關的額外無用功即解析函數。這里不解析函數,該函數聲明了卻沒有指出其用途。所以之前的例子,解析器實際上

原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。

本系列持續更新中,Github 地址請查閱這里。

這是 JavaScript 工作原理的第十四章。

概述

我們都知道運行一大段 JavaScript 代碼性能會變得很糟糕。代碼不僅僅需要在網絡中傳輸而且還需要解析,編譯為字節碼,最后運行。之前的文章討論了諸如 JS 引擎,運行時及調用棧,還有為 Google Chrome 和 NodeJS 廣泛使用的 V8 引擎的話題。它們都在整個 JavaScript 的運行過程中扮演著重要的角色。

今天所講的主題也非常重要:了解到大多數的 JavaScript 引擎是如何把文本解析為機器能夠理解的代碼,轉換之后發生的事情以及開發者如何利用這一知識。

編程語言原理

那么,首先讓我們回顧一下編程語言原理。無論使用何種編程語言,你經常需要一些軟件來處理源碼以便讓計算機能夠理解。該軟件可以是解釋器或編譯器。不管是使用解釋型語言(JavaScript, Python, Ruby) 或者編譯型語言(C#, Java, Rust),它們都有一個共同點:把源碼作為純文本解析為語法抽象樹(AST)的數據結構。AST 不僅要以結構化地方式展示源碼,而且在語義分析中扮演了重要的角色,編譯器檢查驗證程序和語言元素的語法使用是否正確。之后, 使用 AST 來生成實際的字節碼或者機器碼。

AST 程序

AST 不止應用于語言解釋器和編譯器,在計算機世界中,還有其它用途。最為常見的用途之一即靜態代碼分析。靜態代碼分析并不會運行輸入的代碼。但是,它們仍然需要理解代碼的結構。比如,實現一個工具來找出常見的代碼結構以便用來代碼重構減少重復代碼。或許你可以使用字符串比較來實現,但是工具會相當簡單且有局限性。當然了,如果你有興趣實現這樣的工具,你不必自己動手去編寫解析器,有許多完美兼容于 Ecmascript 規范的開源項目。Esprima 和 Acorn 即是黃金搭檔。還有其它工具可以用來幫助解析器輸出代碼,即 ASTs.ASTs 被廣泛應用于代碼轉換。舉個栗子,你可能想實現一個轉換器用來轉換 Python 代碼為 JavaScript.大致的思路即使用 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 Explorer 來查看實際的 AST 樹。這是一個在線工具,你可以在上面寫 JavaScript 代碼,然后網站會輸出目標代碼的 AST。

也許你會問為什么我得學習 JavaScript 解析器的工作原理。反正,瀏覽器會負責運行 JavaScript 代碼。你有那么一丁點是正確的。以下圖表展示了 JavaScript 運行過程中不同階段的耗時。瞪大眼睛瞅瞅,也許你可以發現點有趣的東西。

發現沒?通常情況下,瀏覽器大概消耗了 15% 到 20% 的總運行時間來解析 JavaScript.我沒有具體統計過這些數值。這些統計數據來自于現實世界中程序和網站的各種 JavaScript 使用姿勢。 現在也許 15% 看起來不是很多,但相信我,很多的。一個典型的單頁程序會加載大約 0.4M 的 JavaScript 代碼,然后消耗掉瀏覽器大概 370ms 的時間來進行解析。也許你會又說,這也不是很多嘛。本身花費的時間并不多。但記住了,這只是把 JavaScript 代碼轉化為 ASTs 所消耗的時間。其中不包含運行本身的時間或者頁面加載期間其它諸如 CSS 和 HTML 渲染的過程的耗時。這僅僅只是桌面瀏覽器所面臨的問題。移動瀏覽器的情況會更加復雜。一般情況下,手機移動瀏覽器解析代碼的時間是桌面瀏覽器的 2-5 倍。

以上圖表展示了不同移動和桌面瀏覽器解析 1MB JavaScript 代碼所消耗的時間。

另外,為了獲得更多類原生的用戶體驗而把越來越多的業務邏輯堆積在前端,網頁程序變得越來越復雜。網頁程序越來越胖,都快走不動了。你可以輕易地想到網絡應用受到的性能影響。只需打開瀏覽器開發者工具,然后使用該工具來檢測解析,編譯及其它發生于瀏覽器中直到頁面完全加載所消耗的時間。

不幸的是,移動瀏覽器沒有開發者工具來進行性能檢測。不用擔心。因為有 DeviceTiming 工具。它可以用來幫助檢測受控環境中腳本的解析和運行時間。它通過插入代碼來封裝本地代碼,這樣每當從不同設備訪問的時候,可以本地測量解析和運行時間。

好事即 JavaScript 引擎做了大量的工作來避免冗余工作及更加高效。以下為主流瀏覽器使用的技術。

例如,V8 實現了 script 流和代碼緩存技術。Script 流即當腳本開始下載的時候,async 和 deferred 的腳本在多帶帶的線程中進行解析。這意味著解析會在腳本下載完成時立即完成。這會提升 10% 的頁面加載速度。

每當訪問頁面的時候,JavaScript 代碼通常會被編譯為字節碼。但是,當用戶訪問另一個頁面的時候,該字節碼會作廢。這是因為編譯的代碼嚴重依賴于編譯階段機器的狀態和上下文。從 Chrome 42 開始帶來了字節碼緩存。該技術會本地緩存編譯過的代碼,這樣當用戶返回到同一頁面的時候,諸如下載,解析和編譯等所有步驟都會被跳過。這樣就會為 Chrome 節約大概 40% 的代碼解析和編譯時間。另外,這同樣會節省手機電量。

Opera 中,Carakan 引擎可以復用另一個程序最近編譯過的輸出。不要求代碼在同一頁面或是相同域名下。該緩存技術非常高效且可以完全跳過編譯步驟。它依賴于典型的用戶行為和瀏覽場景:每當用戶在程序/網站上遵循特定的用戶瀏覽習慣,則會加載相同的 JavaScript 代碼。然而,Carakan 早就被谷歌 V8 引擎所取代。

Firefox 使用的 SpiderMonkey 引擎沒有使用任何的緩存技術。它可以過渡到監視階段,在那里記錄腳本運行次數。基于此計算,它推導出頻繁使用而可以被優化的代碼部分。

很明顯地,一些人選擇不做任何處理。Safari 首席開發者 Maciej Stachowiak 指出 Safari 不緩存編譯的字節碼。他們可能已經想到了緩存技術但并沒付諸實施,因為生成代碼的耗時小于總運行時間的 2%。

這些優化措施沒有直接影響 JavaScript 源碼的解析時間,但是會盡可能完全避免。畢竟聊勝于無。

有許多方法可以用來減少程序的初始化加載時間。最小化加載的 JavaScript 數量:代碼越少,解析耗時越少,運行時間越少。為了達到此目的,可以用特殊的方法傳輸必需的代碼而不是一股勞地加載一大坨代碼。比如,PRPL 模式即表示該種代碼傳輸類型。或者,可以檢查依賴然后查看是否有無用、冗余的依賴導致代碼庫的膨脹。然而,這些東西需要很大的篇幅來進行討論。

本文的目標即開發者如何幫助加快 JavaScript 解析器的解析速度。現代 JavaScript 解析器使用 heuristics(啟發法) 來決定是否立即運行指定的代碼片段或者推遲在未來的某個時候運行。基于這些 heuristics,解析器會進行立即或者懶解析。立即解析會運行需要立即編譯的函數。其主要做三件事:構建 AST,構建作用域層級,然后檢查所有的語法錯誤。而懶解析只運行未編譯的函數,它不構建 AST和檢查任何語法錯誤。只構建作用域層級,這樣相對于立即解析會節省大約一半的時間。

顯然,這并不是一個新概念。甚至像 IE9 這樣老掉牙的瀏覽器也支持該優化技術,雖然和現代解析器的工作方式相比是以一種簡陋的方式實現的。

舉個栗子吧。假設有如下代碼片段:

function foo() {
    function bar(x) {
        return x + 10;
    }

    function baz(x, y) {
        return x + y;
    }

    console.log(baz(100, 200));
}

和之前代碼類似,把代碼輸入解析器進行語法分析然后輸出 AST。這樣表述如下:

聲明 bar 函數接收 x 參數。有一個返回語句。函數返回 x 和 10 相加的結果。

聲明 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;
};

注意,在 function 關鍵字和函數參數的左括號之間的函數名。這并不是必要的,但推薦這樣做,因為當拋出異常錯誤的時候,堆棧追蹤會包含實際的函數名而不是

解析器仍然會做懶解析。可以做一個微小的改動來解決這一問題:用括號封裝函數。

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 團隊已經在內部解決重復解析問題,這樣預編譯有可能實際上沒啥鳥用。

一些提升網絡應用速度的建議

檢查依賴。減少不必要的依賴。

分割代碼為更小的塊而不是一整塊。如 webpack 的 code-spliting 功能。

盡可能延遲加載 JavaScript 代碼。可以只加載當前路由所要求的代碼片段。比如只在點擊某個元素的時候引入 某段代碼模塊。

使用開發者工具和 DeviceTiming 來檢測性能瓶頸。

使用像 Optimize.js 的工具來幫助解析器選擇立即解析或者懶解析以加快解析速度。

拓展

有時候,特別是手機端瀏覽器,比如當你點擊前進/后退按鈕的時候,瀏覽器會進行緩存。但是在有些場景下,你可能不需要瀏覽器的這種功能。有如下解決辦法:

window.addEventListener("pageshow", (event) => {
  // 檢查前進/后退緩存,是否從緩存加載頁面
  if (event.persisted || window.performance && 
    window.performance.navigation.type === 2) {
    // 進行相應的邏輯處理
  }
};

本系列持續更新中,Github 地址請查閱這里。

文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。

轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/116892.html

相關文章

  • JavaScript 工作原理十四解析語法抽象樹及小化解析時間 5 條小技巧

    摘要:事實是只是部分語言的不同表示法。基于這些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關的額外無用功即解析函數。這里不解析函數,該函數聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...

    ZweiZhao 評論0 收藏0
  • JavaScript 工作原理十四解析語法抽象樹及小化解析時間 5 條小技巧

    摘要:事實是只是部分語言的不同表示法。基于這些,解析器會進行立即或者懶解析。然而,解析器做了完全不相關的額外無用功即解析函數。這里不解析函數,該函數聲明了卻沒有指出其用途。所以之前的例子,解析器實際上 原文請查閱這里,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第十四章。 概...

    xuxueli 評論0 收藏0
  • JavaScript 工作原理之十五-類和繼承及 Babel 和 TypeScript 代碼轉換探秘

    摘要:使用新的易用的類定義,歸根結底也是要創建構造函數和修改原型。首先,它把構造函數當成單獨的函數且包含類屬性集。該節點還儲存了指向父類的指針引用,該父類也并儲存了構造函數,屬性集和及父類引用,依次類推。 原文請查閱這里,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第...

    GeekGhc 評論0 收藏0
  • JavaScript 工作原理之十五-類和繼承及 Babel 和 TypeScript 代碼轉換探秘

    摘要:使用新的易用的類定義,歸根結底也是要創建構造函數和修改原型。首先,它把構造函數當成單獨的函數且包含類屬性集。該節點還儲存了指向父類的指針引用,該父類也并儲存了構造函數,屬性集和及父類引用,依次類推。 原文請查閱這里,略有刪減,本文采用知識共享署名 4.0 國際許可協議共享,BY Troland。 本系列持續更新中,Github 地址請查閱這里。 這是 JavaScript 工作原理的第...

    BigNerdCoding 評論0 收藏0

發表評論

0條評論

最新活動
閱讀需要支付1元查看
<