自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

從JavaScript的運(yùn)行原理談解析效率優(yōu)化

開發(fā) 前端
在這篇文章中,我們將重點(diǎn)介紹第一階段并解釋該階段對(duì)編寫高效 JavaScript 的影響。我們會(huì)按照從左到右、從上到下的順序介紹解析管道,該管道接受源代碼并生成一棵語(yǔ)法樹。

[[312684]]

編寫高效率的 JavaScript ,其中一個(gè)關(guān)鍵就是要理解它的工作原理。編寫高效代碼的方法數(shù)不勝數(shù),例如,你可以編寫對(duì)編譯器友好的 JavaScript 代碼,從而避免將一行簡(jiǎn)單代碼的運(yùn)行速度拖慢 7 倍。

本文我們會(huì)專注講解可以最小化 Javascript 代碼解析時(shí)間的優(yōu)化方法。我們進(jìn)一步縮小范圍,只討論 V8 這一驅(qū)動(dòng) ElectronNode.jsGoogle Chrome 的 JS 引擎。為了理解這些對(duì)解析友好的優(yōu)化方法,我們還得先討論 JavaScript 的解析過程,在深入理解代碼解析過程的基礎(chǔ)上,再對(duì)三個(gè)編寫更高速 JavaScript 的技巧進(jìn)行一一概述。

先簡(jiǎn)單回顧一下 JavaScript 執(zhí)行的三個(gè)階段。

  1.  從源代碼到語(yǔ)法樹 —— 解析器從源碼中生成一棵 抽象語(yǔ)法樹。
  2.  從語(yǔ)法樹到字節(jié)碼 —— V8 的解釋器 Ignition 從語(yǔ)法樹中生成字節(jié)碼(在 2017 年之前 并沒有該步驟,具體可以看 這篇文章)。
  3.  從字節(jié)碼到機(jī)器碼 —— V8 的編譯器 TurboFan 從字節(jié)碼中生成圖,用高度優(yōu)化的機(jī)器碼替代部分字節(jié)碼。

上述的第二和第三階段 涉及到了 JavaScript 的編譯。在這篇文章中,我們將重點(diǎn)介紹第一階段并解釋該階段對(duì)編寫高效 JavaScript 的影響。我們會(huì)按照從左到右、從上到下的順序介紹解析管道,該管道接受源代碼并生成一棵語(yǔ)法樹。

抽象語(yǔ)法樹(AST)。它是在解析器(圖中藍(lán)色部分)中創(chuàng)建的。

掃描器

源代碼首先被分解成 chunk,每個(gè) chunk 都可能采用不同的編碼,稍后會(huì)有一個(gè)字符流將所有 chunk 的編碼統(tǒng)一為 UTF-16。

在解析之前,掃描器會(huì)將 UTF-16 字符流分解成 token。token 是一段腳本中具有語(yǔ)義的最小單元。有不同類型的 token,包括空白符(用于 自動(dòng)插入分號(hào))、標(biāo)識(shí)符、關(guān)鍵字以及代理對(duì)(僅當(dāng)代理對(duì)無法被識(shí)別為其它東西時(shí)才會(huì)結(jié)合成標(biāo)識(shí)符)。這些 token 之后被送往預(yù)解析器中,接著再送往解析器。

預(yù)解析器

解析器的工作量是最少的,只要足夠跳過傳入的源代碼并進(jìn)行懶解析(而不是全解析)即可。預(yù)解析器確保輸入的源代碼包含有效語(yǔ)法,并生成足夠的信息來正確地編譯外部函數(shù)。這個(gè)準(zhǔn)備好的函數(shù)稍后將按需編譯。

解析

解析器接收到掃描器生成的 token 后,現(xiàn)在需要生成一個(gè)供編譯器使用的中間表示。

首先我們來討論解析樹。解析樹,或者說 具體語(yǔ)法樹(CST)將源語(yǔ)法表示為一棵樹。每個(gè)葉子節(jié)點(diǎn)都是一個(gè) token,而每個(gè)中間節(jié)點(diǎn)則表示一個(gè)語(yǔ)法規(guī)則。在英語(yǔ)里,語(yǔ)法規(guī)指的是名詞、主語(yǔ)等,而在編程里,語(yǔ)法規(guī)則指的是一個(gè)表達(dá)式。不過,解析樹的大小隨著程序大小會(huì)增長(zhǎng)得很快。

相反,抽象語(yǔ)法樹 要更加簡(jiǎn)潔。每個(gè)中間節(jié)點(diǎn)表示一個(gè)結(jié)構(gòu),比如一個(gè)減法運(yùn)算(-),并且這棵樹并沒有展示源代碼的所有細(xì)節(jié)。例如,由括號(hào)定義的分組是蘊(yùn)含在樹的結(jié)構(gòu)中的。另外,標(biāo)點(diǎn)符號(hào)、分隔符以及空白符都被省略了。你可以在 這里 了解更多 AST 和 CST 的區(qū)別。

接下來我們將重點(diǎn)放在 AST 上。以下面用 JavaScript 編寫的斐波那契程序?yàn)槔?nbsp;

  1. function fib(n) {   
  2.   if (n <= 1) return n;   
  3.   return fib(n-1) + fib(n-2);   
  4.   } 

下面的 JSON 文件就是對(duì)應(yīng)的抽象語(yǔ)法

了。這是用 AST Explorer 生成的。(如果你不熟悉這個(gè),可以點(diǎn)擊這里來詳細(xì)了解 如何閱讀 JSON 格式的 AST)。 

  1.  
  2.   "type": "Program",  
  3.   "start": 0,  
  4.   "end": 73,  
  5.   "body": [  
  6.     {  
  7.       "type": "FunctionDeclaration",  
  8.       "start": 0,  
  9.       "end": 73,  
  10.       "id": {  
  11.         "type": "Identifier",  
  12.         "start": 9,  
  13.         "end": 12,  
  14.         "name": "fib"  
  15.       },  
  16.       "expression": false,  
  17.       "generator": false,  
  18.       "async": false,  
  19.       "params": [  
  20.         {  
  21.           "type": "Identifier",  
  22.           "start": 13,  
  23.           "end": 14,  
  24.           "name": "n"  
  25.         }  
  26.       ],  
  27.       "body": {  
  28.         "type": "BlockStatement",  
  29.         "start": 16,  
  30.         "end": 73,  
  31.         "body": [  
  32.           {  
  33.             "type": "IfStatement",  
  34.             "start": 20,  
  35.             "end": 41,  
  36.             "test": {  
  37.               "type": "BinaryExpression",  
  38.               "start": 24,  
  39.               "end": 30,  
  40.               "left": {  
  41.                 "type": "Identifier",  
  42.                 "start": 24,  
  43.                 "end": 25,  
  44.                 "name": "n"  
  45.               },  
  46.               "operator": "<=",  
  47.               "right": {  
  48.                 "type": "Literal",  
  49.                 "start": 29,  
  50.                 "end": 30,  
  51.                 "value": 1,  
  52.                 "raw": "1"  
  53.               }  
  54.             },  
  55.             "consequent": {  
  56.               "type": "ReturnStatement",  
  57.               "start": 32,  
  58.               "end": 41,  
  59.               "argument": {  
  60.                 "type": "Identifier",  
  61.                 "start": 39,  
  62.                 "end": 40,  
  63.                 "name": "n"  
  64.               }  
  65.             },  
  66.             "alternate": null  
  67.           },  
  68.           {  
  69.             "type": "ReturnStatement",  
  70.             "start": 44,  
  71.             "end": 71,  
  72.             "argument": {  
  73.               "type": "BinaryExpression",  
  74.               "start": 51,  
  75.               "end": 70,  
  76.               "left": {  
  77.                 "type": "CallExpression",  
  78.                 "start": 51,  
  79.                 "end": 59,  
  80.                 "callee": {  
  81.                   "type": "Identifier",  
  82.                   "start": 51,  
  83.                   "end": 54,  
  84.                   "name": "fib"  
  85.                 },  
  86.                 "arguments": [  
  87.                   {  
  88.                     "type": "BinaryExpression",  
  89.                     "start": 55,  
  90.                     "end": 58,  
  91.                     "left": {  
  92.                       "type": "Identifier",  
  93.                       "start": 55,  
  94.                       "end": 56,  
  95.                       "name": "n"  
  96.                     },  
  97.                     "operator": "-",  
  98.                     "right": {  
  99.                       "type": "Literal",  
  100.                       "start": 57,  
  101.                       "end": 58,  
  102.                       "value": 1,  
  103.                       "raw": "1"  
  104.                     }  
  105.                   }  
  106.                 ]  
  107.               },  
  108.               "operator": "+",  
  109.               "right": {  
  110.                 "type": "CallExpression",  
  111.                 "start": 62,  
  112.                 "end": 70,  
  113.                 "callee": {  
  114.                   "type": "Identifier",  
  115.                   "start": 62,  
  116.                   "end": 65,  
  117.                   "name": "fib"  
  118.                 },  
  119.                 "arguments": [  
  120.                   {  
  121.                     "type": "BinaryExpression",  
  122.                     "start": 66,  
  123.                     "end": 69,  
  124.                     "left": {  
  125.                       "type": "Identifier",  
  126.                       "start": 66,  
  127.                       "end": 67,  
  128.                       "name": "n"  
  129.                     },  
  130.                     "operator": "-",  
  131.                     "right": {  
  132.                       "type": "Literal",  
  133.                       "start": 68,  
  134.                       "end": 69,  
  135.                       "value": 2,  
  136.                       "raw": "2"  
  137.                     }  
  138.                   }  
  139.                 ]  
  140.               }  
  141.             }  
  142.           }  
  143.         ]  
  144.       }  
  145.     }  
  146.   ],  
  147.   "sourceType": "module"  
  148.  
  149. (來源:GitHub) 

上面代碼的要點(diǎn)是,每個(gè)非葉子節(jié)點(diǎn)都是一個(gè)運(yùn)算符,而每個(gè)葉子節(jié)點(diǎn)都是操作數(shù)。這棵語(yǔ)法樹稍后將作為輸入傳給 JavaScript 接著要執(zhí)行的兩個(gè)階段。

三個(gè)技巧優(yōu)化你的 JavaScript

下面羅列的技巧清單中,我會(huì)省略那些已經(jīng)廣泛使用的技巧,例如縮減代碼來最大化信息密度,從而使掃描器更具有時(shí)效性。另外,我也會(huì)跳過那些適用范圍很小的建議,例如避免使用非 ASCII 字符。

提高解析性能的方法數(shù)不勝數(shù),讓我們著眼于其中適用范圍最廣泛的方法吧。

1.盡可能遵從工作線程

主線程被阻塞會(huì)導(dǎo)致用戶交互的延遲,所以應(yīng)該盡可能減少主線程上的工作。關(guān)鍵就是要識(shí)別并避免會(huì)導(dǎo)致主線程中某些任務(wù)長(zhǎng)時(shí)間運(yùn)行的解析行為。

這種啟發(fā)式超出了解析器的優(yōu)化范圍。例如,用戶控制的 JavaScript 代碼段可以使用 web workers 達(dá)到相同的效果。你可以閱讀 實(shí)時(shí)處理應(yīng)用在 angular 中使用 web workers 來了解更多信息。

避免使用大量的內(nèi)聯(lián)腳本

內(nèi)聯(lián)腳本是在主線程中處理的,根據(jù)之前的說法,應(yīng)該盡量避免這樣做。事實(shí)上,除了異步和延遲加載之外,任何 JavaScript 的加載都會(huì)阻塞主線程。

避免嵌套外層函數(shù)

懶編譯也是發(fā)生在主線程上的。不過,如果處理得當(dāng)?shù)脑?,懶解析可以加快啟?dòng)速度。想要強(qiáng)制進(jìn)行全解析的話,可以使用諸如 optimize.js(已經(jīng)不維護(hù))這樣的工具來決定進(jìn)行全解析或者懶解析。

分解超過 100kB 的文件

將大文件分解成小文件以最大化并行腳本的加載速度。“2019 年 JavaScript 的性能開銷”一文比較了 Facebook 網(wǎng)站和 Reddit 網(wǎng)站的文件大小。前者通過在 300 多個(gè)請(qǐng)求中拆分大約 6MB 的 JavaScript ,成功將解析和編譯工作在主線程上的占比控制到 30%;相反,Reddit 的主線程上進(jìn)行解析和編譯工作的達(dá)到了將近 80%。

2. 使用 JSON 而不是對(duì)象字面量 —— 偶爾

在 JavaScript 中,解析 JSON 比解析對(duì)象字面量來得更加高效。 parsing benchmark 已經(jīng)證實(shí)了這一點(diǎn)。在不同的主流 JavaScript 執(zhí)行引擎中分別解析一個(gè) 8MB 大小的文件,前者的解析速度最高可以提升 2 倍。

2019 年谷歌開發(fā)者大會(huì) 也討論過 JSON 解析如此高效的兩個(gè)原因:

  1.  JSON 是單字符串 token,而對(duì)象字面量可能包含大量的嵌套對(duì)象和 token;
  2.  語(yǔ)法對(duì)上下文是敏感的。解析器逐字檢查源代碼,并不知道某個(gè)代碼塊是一個(gè)對(duì)象字面量。而左大括號(hào)不僅可以表明它是一個(gè)對(duì)象字面量,還可以表明它是一個(gè)解構(gòu)對(duì)象或者箭頭函數(shù)。

不過,值得注意的是,JSON.parse 同樣會(huì)阻塞主線程。對(duì)于超過 1MB 的文件,可以使用 FlatBuffers 提高解析效率

3. 最大化代碼緩存

最后,你可以通過完全規(guī)避解析來提高解析效率。對(duì)于服務(wù)端編譯來說, WebAssembly (WASM) 是個(gè)不錯(cuò)的選擇。然而,它沒辦法替代 JavaScript。對(duì)于 JS,更合適的方法是最大化代碼緩存。

值得注意的是,緩存并不是任何時(shí)候都生效的。在執(zhí)行結(jié)束之前編譯的任何代碼都會(huì)被緩存 —— 這意味著處理器、監(jiān)聽器等不會(huì)被緩存。為了最大化代碼緩存,你必須最大化執(zhí)行結(jié)束之前編譯的代碼數(shù)量。其中一個(gè)方法就是使用立即執(zhí)行函數(shù)(IIFE)啟發(fā)式:解析器會(huì)通過啟發(fā)式的方法標(biāo)識(shí)出這些 IIFE 函數(shù),它們會(huì)在稍后立即被編譯。因此,使用啟發(fā)式的方法可以確保一個(gè)函數(shù)在腳本執(zhí)行結(jié)束之前被編譯。

此外,緩存是基于單個(gè)腳本執(zhí)行的。這意味著更新腳本將會(huì)使緩存失效。V8 團(tuán)隊(duì)建議可以分割腳本或者合并腳本,從而實(shí)現(xiàn)代碼緩存。但是,這兩個(gè)建議是互相矛盾的。你可以閱讀“JavaScript 開發(fā)中的代碼緩存”來了解更多代碼緩存相關(guān)的信息。

結(jié)論

解析時(shí)間的優(yōu)化涉及到工作線程的延遲解析以及通過最大化緩存來避免完全解析。理解了 V8 的解析機(jī)制后,我們也能推斷出上面沒有提到的其它優(yōu)化方法。

下面給出了更多了解解析機(jī)制的資源,這個(gè)機(jī)制通常來說同時(shí)適用于 V8 和 JavaScript 的解析。

額外小貼士:理解 JavaScript 的錯(cuò)誤和性能是如何影響你的用戶的。

跟蹤生產(chǎn)過程中 JavaScript 的異?;蛘咤e(cuò)誤是很耗時(shí)的,而且也很令人傷腦筋。如果你有興趣監(jiān)控 JavaScript 的錯(cuò)誤和應(yīng)用性能是如何對(duì)用戶造成影響的,可以嘗試使用 LogRocket。

LogRocket 就像是為 web 應(yīng)用量身訂造的 DVR(錄像機(jī)),它可以確切地記錄你的網(wǎng)站上發(fā)生的所有事情。LogRocket 可以幫助你統(tǒng)計(jì)并報(bào)告錯(cuò)誤,以查看錯(cuò)誤發(fā)生的頻率以及它們對(duì)你的用戶群的影響程度。你可以輕松地重現(xiàn)錯(cuò)誤發(fā)生時(shí)特定的用戶會(huì)話,以查看是用戶的哪些操作導(dǎo)致了 bug。

LogRocket 可以記錄你的 app 上的請(qǐng)求和響應(yīng)(包含 header 和 body)以及用戶相關(guān)的上下文信息,從而窺探問題全貌。它也可以記錄頁(yè)面的 HTML 和 CSS,即使是面對(duì)最復(fù)雜的單頁(yè)面應(yīng)用,也可以重構(gòu)出像素完美級(jí)別的視頻。

如果你想提高你的 JavaScript 錯(cuò)誤監(jiān)控能力,LogRocket 是個(gè)不錯(cuò)的選擇。 

 

責(zé)任編輯:龐桂玉 來源: segmentfault
相關(guān)推薦

2019-12-06 10:59:20

JavaScript運(yùn)行引擎

2025-04-02 07:29:14

2011-07-11 15:36:44

JavaScript

2009-11-13 13:48:58

網(wǎng)絡(luò)配置DNS

2010-05-20 18:40:33

IIS服務(wù)器

2009-11-13 10:48:47

網(wǎng)絡(luò)配置DNS

2020-12-20 10:02:17

ContextReactrender

2015-03-10 13:55:31

JavaScript預(yù)解析原理及實(shí)現(xiàn)

2019-09-18 08:53:55

2020-01-06 11:22:06

TCPLinux內(nèi)核

2011-12-19 09:23:50

UML建模

2025-01-10 11:28:58

2011-04-28 16:36:17

投影機(jī)

2010-03-15 14:01:26

JavaScript

2020-08-13 11:24:45

Java技術(shù)開發(fā)

2024-07-07 21:49:22

2024-06-27 11:22:34

2011-06-16 14:38:18

JavaScript事件委托

2016-10-21 11:04:07

JavaScript異步編程原理解析

2017-05-31 13:16:35

PHP運(yùn)行機(jī)制原理解析
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)