1. 程式人生 > >解讀JavaScript 之引擎、運行時和堆棧調用

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

trace 問題 www. 個數 是否 任務 out translate 機制

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

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

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

本文是該系列文章的第一篇,旨在深入研究 JavaScript 及其實際工作原理:我們認為通過了解 JavaScript 的構建塊以及它們如何一起協作的,你將能夠編寫更好的代碼和應用。

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

技術分享圖片

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

如果項目越來越依賴於 JavaScript ,這意味著開發人員必須利用語言和生態系統所提供的所有內容,深入了解其內部,從而構建出令人驚嘆的軟件。

事實證明,很多開發人員每天都在使用 JavaScript ,但他們並不知道底層會發生什麽。

概述

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

在這篇文章中,我們將詳細介紹所有這些概念,並解釋 JavaScript 是如何運行的。通過了解這些細節,你將能夠編寫更好的、非阻塞的應用程序,正確使用所提供的 API 。

如果你對 JavaScript 比較生疏,本博客文章將幫助你理解為什麽 JavaScript 相比與其他語言更“怪異”。

如果你是一位經驗豐富的 JavaScript 開發人員,希望能夠為你提供一些關於你每天使用的 JavaScript 運行時的實際工作情況的全新見解。

JavaScript 引擎

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

技術分享圖片

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

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

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

Runtime 運行時

幾乎所有的 JavaScript 開發者都使用過瀏覽器中的 API(例如“setTimeout”)。 但是,這些 API 不是由引擎提供的。

那麽,它們從哪裏來呢?

事實證明,實際情況有點復雜。

技術分享圖片

所以,我們有引擎,但實際上還有更多。我們有那些由瀏覽器所提供的稱為 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 將會變成空的。之後,執行的步驟如下:

技術分享圖片

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:

技術分享圖片

“Blowing the stack”—當達到最大調用堆棧大小時,會發生這種情況。這可能會很容易發生,特別是如果你使用遞歸,而不是非常廣泛地測試你的代碼。看看這個示例代碼:

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

當引擎開始執行這個代碼時,它首先調用函數“foo”。然而,這個函數是遞歸的,並且開始調用自己而沒有任何終止條件。所以在執行的每個步驟中,同一個函數會一次又一次地添加到調用堆棧中。它看起來像這樣:
技術分享圖片
然而,在某些情況下,調用堆棧中函數調用的數量超出了調用堆棧的實際大小,瀏覽器通過拋出一個錯誤(如下所示)來決定采取行動:
技術分享圖片
在單線程上運行代碼可能非常容易,因為你不必處理多線程環境中出現的復雜場景,例如死鎖。

但是在單線程上運行也是非常有限的。由於JavaScript只有一個調用堆棧,所以當事情很慢時會發生什麽?

並發&事件循環

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

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

而且這並不是唯一的問題。一旦你的瀏覽器開始在 Call Stack 中處理過多的任務,它可能會停止響應相當長的時間。大多數瀏覽器會通過觸發錯誤來采取行動,詢問你是否要終止網頁。

技術分享圖片

所以,這並不是最好的用戶體驗,對嗎?

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

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