解讀 JavaScript 之引擎、執行時和堆疊呼叫

森林小獵人發表於2019-04-10

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

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

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

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

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

img

(檢視 GitHub 語言統計最新版)

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

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

概述

幾乎每個人都已經聽說過 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 使得瀏覽器無法響應? 解決方案就是非同步回撥。

這將在“ JavaScript 工作原理”教程的第2部分中更詳細地解釋:“V8 引擎內部+關於如何編寫優化程式碼的5個技巧”。

同時,如果你在 JavaScript 應用程式中難以復現和理解問題,請檢視 SessionStack 。 SessionStack 會記錄你的 Web 應用中的所有東西:所有的 DOM 更改、使用者互動、JavaScript 異常、堆疊跟蹤、網路請求失敗、除錯訊息等。

通過 SessionStack ,你可以以視訊的方式重現問題,並檢視發生在使用者身上的所有事情。

這有一個免費的方案,所以你可以試試看

img

相關文章