解讀JavaScript 之引擎、運行時和堆棧調用

轉載自開源中國 譯者:Tocy, 涼涼_, 亞林瓜子, 離謅 原文連接javascript

英文原文:How JavaScript works: an overview of the engine, the runtime, and the call stack前端

隨着 JavaScript 變得愈來愈流行,不少團隊在他們的堆棧中實現諸多層級的支持 - 前端、後端、混合應用程序、嵌入式設備等等。java

本文是該系列文章的第一篇,旨在深刻研究 JavaScript 及其實際工做原理:咱們認爲經過了解 JavaScript 的構建塊以及它們如何一塊兒協做的,你將可以編寫更好的代碼和應用。git

GitHut 統計中所示,JavaScript 在 GitHub 中的活動存儲庫和總推送量方面位居前列。但它在其餘分類中也未落後太多。編程

img

(查看 GitHub 語言統計最新版)後端

若是項目愈來愈依賴於 JavaScript ,這意味着開發人員必須利用語言和生態系統所提供的全部內容,深刻了解其內部,從而構建出使人驚歎的軟件。瀏覽器

事實證實,不少開發人員天天都在使用 JavaScript ,但他們並不知道底層會發生什麼。session

概述

幾乎每一個人都已經據說過 V8 引擎這個概念,大多數人都知道 JavaScript 是單線程的,或者它正在使用回調隊列。數據結構

在這篇文章中,咱們將詳細介紹全部這些概念,並解釋 JavaScript 是如何運行的。經過了解這些細節,你將可以編寫更好的、非阻塞的應用程序,正確使用所提供的 API 。多線程

若是你對 JavaScript 比較生疏,本博客文章將幫助你理解爲何 JavaScript 相比與其餘語言更「怪異」。

若是你是一位經驗豐富的 JavaScript 開發人員,但願可以爲你提供一些關於你天天使用的 JavaScript 運行時的實際工做狀況的全新看法。

JavaScript 引擎

Google V8 引擎是一個比較流行的 JavaScript 引擎示例。V8 引擎是在諸如 Chrome 和 Node.js 等內部使用的。下面是對其機制的一個簡化視圖:

img

該引擎包括兩個主要組件:

* Memory Heap 內存堆 ——  這是內存分配發生的地方

* Call Stack 調用堆棧 ——  這是在你代碼執行時棧幀存放的位置

Runtime 運行時

幾乎全部的 JavaScript 開發者都使用過瀏覽器中的 API(例如「setTimeout」)。 可是,這些 API 不是由引擎提供的。

那麼,它們從哪裏來呢?

事實證實,實際狀況有點複雜。

img

因此,咱們有引擎,但實際上還有更多。咱們有那些由瀏覽器所提供的稱爲 Web API 的東西,好比 DOM、AJAX、setTimeout 等等。

而後,咱們還有很是流行的事件循環和回調隊列

Call Stack 調用堆棧

JavaScript 是一種單線程編程語言,這意味着它只有一個 Call Stack 。所以,它一次僅能作一件事。

Call Stack 是一個數據結構,它基本上記錄了咱們在程序中的所處的位置。若是咱們進入一個函數,咱們把它放在堆棧的頂部。若是咱們從一個函數中返回,咱們彈出堆棧的頂部。這是全部的堆棧能夠作的東西。

咱們來看一個例子。看看下面的代碼:

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

當引擎開始執行這個代碼時,Call Stack 將會變成空的。以後,執行的步驟以下:

img

Call Stack 的每一個入口被稱爲 Stack Frame(棧幀)。

這正是在拋出異常時如何構建 stack trace 的方法 - 這基本上是在異常發生時的 Call Stack 的狀態。看看下面的代碼:

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

若是這是在 Chrome 中執行的(假設這個代碼在一個名爲 foo.js 的文件中),那麼會產生下面的 stack trace:

img

「Blowing the stack」—當達到最大調用堆棧大小時,會發生這種狀況。這可能會很容易發生,特別是若是你使用遞歸,而不是很是普遍地測試你的代碼。看看這個示例代碼:

function foo() {
    foo();
}
foo();

當引擎開始執行這個代碼時,它首先調用函數「foo」。然而,這個函數是遞歸的,而且開始調用本身而沒有任何終止條件。因此在執行的每一個步驟中,同一個函數會一次又一次地添加到調用堆棧中。它看起來像這樣:
img
然而,在某些狀況下,調用堆棧中函數調用的數量超出了調用堆棧的實際大小,瀏覽器經過拋出一個錯誤(以下所示)來決定採起行動:
img
在單線程上運行代碼可能很是容易,由於你沒必要處理多線程環境中出現的複雜場景,例如死鎖。

可是在單線程上運行也是很是有限的。因爲JavaScript只有一個調用堆棧,因此當事情很慢時會發生什麼?

併發&事件循環

若是在調用堆棧中執行的函數調用須要花費大量時間才能進行處理,會發生什麼? 例如,假設你想在瀏覽器中使用 JavaScript 進行一些複雜的圖像轉換。

你可能會問 - 爲何這會是一個問題?問題是,雖然調用堆棧有要執行的函數,瀏覽器實際上不能作任何事情 - 它被阻塞了。這意味着瀏覽器沒法渲染,它不能運行任何其餘代碼,它就是被卡住了。若是你想在你的應用程序中使用流暢的 UI ,這就會產生問題。

並且這並非惟一的問題。一旦你的瀏覽器開始在 Call Stack 中處理過多的任務,它可能會中止響應至關長的時間。大多數瀏覽器會經過觸發錯誤來採起行動,詢問你是否要終止網頁。

img

因此,這並非最好的用戶體驗,對嗎?

那麼,咱們如何執行大量代碼而不阻塞 UI 使得瀏覽器沒法響應? 解決方案就是異步回調。

相關文章
相關標籤/搜索