[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

小烜同學發表於2017-11-12

原文地址: https://blog.sessionstack.com/how-does-javascript-actually-work-part-1-b0bacc073cf

PS: 好久沒寫東西了,最近一直在準備寫一個自己的部落格,最後一些技術方向已經敲定了,又可以開心的學習了,node系列後續再開始。


  隨著JavaScript越來越流行,越來越多的團隊廣泛的把JavaScript應用到前端、後臺、hybrid 應用、嵌入式等等領域。

  這篇文章旨在深入挖掘JavaScript,以及向大家解釋JavaScript是如何工作的。我們通過了解它的底層構建以及它是怎麼發揮作用的,可以幫助我們寫出更好的程式碼與應用。據 GitHut 統計顯示,JavaScript 長期佔據GitHub中 Active RepositoriesTotal Pushes 的榜首,並且在其他的類別中也不會落後太多。

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  如果一個專案越來越依賴 JavaScript,這就意味著開發人員必須利用這些語言和生態系統提供更深層次的核心內容去構建一個令人振奮的應用。然而,事實證明,有很多的開發者每天都在使用 JavaScript,但是卻不知道在底層 JavaScript 是怎麼運作的。

概述

  幾乎每個人聽說過 V8 引擎的概念,而且,大多數人都知道 JavaScript 是單執行緒的,或者是它是使用回撥佇列的。

  在這篇文章中,我們將詳細的介紹這些概念,並解釋 JavaScript 是怎麼工作的。通過了解這些細節,你就能利用這些提供的 API 來寫出更好的,非阻塞的應用來。如果你對 JavaScript 比較陌生,那麼這篇文章將幫助您理解為什麼 JavaScript 相較於其他語言顯得如此“怪異”。如果您是一位經驗豐富的 JavaScript 開發人員,希望它能給你帶來一些新的見解,說明 JavaScript 的執行時,儘管你可能每天都會用到它。

JavaScript 引擎

  JavaScript 引擎說起來最流行的當然是谷歌的 V8 引擎了, V8 引擎使用在 Chrome 以及 Node 中,下面有個簡單的圖能說明他們的關係:

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  這個引擎主要由兩部分組成:

  • 記憶體堆:這是記憶體分配發生的地方
  • 呼叫棧:這是你的程式碼執行時的地方

執行時

  有些瀏覽器的 API 經常被使用到(比如說:setTimeout),但是,這些 API 卻不是引擎提供的。那麼,他們是從哪兒來的呢?事實上這裡面實際情況有點複雜。

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  所以說我們還有很多引擎之外的 API,我們把這些稱為瀏覽器提供的 Web API,比如說 DOM、AJAX、setTimeout等等。

  然後我們還擁有如此流行的事件迴圈和回撥佇列。

呼叫棧

  JavaScript 是一門單執行緒的語言,這意味著它只有一個呼叫棧,因此,它同一時間只能做一件事。

  呼叫棧是一種資料結構,它記錄了我們在程式中的位置。如果我們執行到一個函式,它就會將其放置到棧頂。當從這個函式返回的時候,就會將這個函式從棧頂彈出,這就是呼叫棧做的事情。

  讓我們來看一看下面的例子:

    function multiply(x, y) {
      return x * y;
    }
    function printSquare(x) {
      var s = multiply(x, x);
      console.log(s);
    }
    printSquare(5);
複製程式碼

  當程式開始執行的時候,呼叫棧是空的,然後,步驟如下:

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  每一個進入呼叫棧的都稱為__呼叫幀__。

  這能清楚的知道當異常發生的時候堆疊追蹤是怎麼被構造的,堆疊的狀態是如何的。讓我們看一下下面的程式碼:

    function foo() {
      throw new Error('SessionStack will help you resolve crashes :)');
    }
    function bar() {
      foo();
    }
    function start() {
      bar();
    }
    start();
複製程式碼

  如果這發生在 Chrome 裡(假設這段程式碼實在一個名為 foo.js 的檔案中),那麼將會生成以下的堆疊追蹤:

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  "堆疊溢位",當你達到呼叫棧最大的大小的時候就會發生這種情況,而且這相當容易發生,特別是在你寫遞迴的時候卻沒有全方位的測試它。我們來看看下面的程式碼:

    function foo() {
      foo();
    }
    foo();
複製程式碼

  當我們的引擎開始執行這段程式碼的時候,它從 foo 函式開始。然後這是個遞迴的函式,並且在沒有任何的終止條件的情況下開始呼叫自己。因此,每執行一步,就會把這個相同的函式一次又一次地新增到呼叫堆疊中。然後它看起來就像是這樣的:

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  然後,在某一時刻,呼叫棧中的函式呼叫的數量超過了呼叫棧的實際大小,瀏覽器決定幹掉它,丟擲一個錯誤,它看起來就像是這樣:

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  在單個執行緒上執行程式碼很容易,因為你不必處理在多執行緒環境中出現的複雜場景——例如死鎖。但是在一個執行緒上執行也非常有限制。由於 JavaScript 只有一個呼叫堆疊,當某段程式碼執行變慢時會發生什麼?

併發與事件迴圈

  呼叫棧中的函式呼叫需要大量的時間來處理,那麼這會發生什麼情況呢?例如,假設你想在瀏覽器中使用 JavaScript 進行一些複雜的圖片轉碼。

  你可能會問?這算什麼問題?事實上,問題是當呼叫棧有函式要執行,瀏覽器就不能做任何事,它會被堵塞住。這意味著瀏覽器不能渲染,不能執行其他的程式碼,它被卡住了。如果你想在應用裡讓 UI 很流暢的話,這就會產生問題。

  而且這不是唯一的問題,一旦你的瀏覽器開始處理呼叫棧中的眾多工,它可能會停止響應相當長一段時間。大多數瀏覽器都會這麼做,報一個錯誤,詢問你是否想終止 web 頁面。

[譯] JavaScript 如何工作:對引擎、執行時、呼叫堆疊的概述

  這樣看來,這並不是最好的使用者體驗,不是嗎?

  那麼,如何在不阻塞 UI 的情況下執行復雜的程式碼,讓瀏覽器不會不響應?解決方案就是非同步回撥。這將在“ JavaScript 如何工作”教程的第2部分中詳細解釋:“在V8引擎中,如何編寫優化程式碼”。

相關文章