1. 程式人生 > >JavaScript 如何工作系列: 引擎、執行時、呼叫棧概述

JavaScript 如何工作系列: 引擎、執行時、呼叫棧概述

譯者: 波比小金剛

翻譯水平有限,如有錯誤請指出。

原文: blog.sessionstack.com/how-does-ja…

ps: 最近開始整理所有的優質文章翻譯集,當然如果你有好的文章請提 issue,我會找時間翻譯出來。


JavaScript 越來越流行,在前端、後端、hybrid apps、嵌入式裝置開發等方向上都有它活躍的身影。

這篇文章是 How JavaScript Works 系列的開篇,該系列的文章旨在深入挖掘 JavaScript 及其實際的工作原理。我們認為了解 JavaScript 的構建塊及其共同作用,可以幫助我們寫出更優雅、更高效的程式碼和應用。

正如 GitHut stats 所展示的一樣,JavaScript 各方面的統計資料都是棒棒噠,頂多也就在個別統計項上落後了其他語言那麼一丟丟。

GitHut stats

如果專案深度依賴 JavaScript,這意味著開發者需要對底層有極其深入的瞭解,並利用語言和生態提供的一切東西來構建出色的應用。

然而,事實上,很多開發者雖然每天都在使用 JavaScript,卻對其背後發生的事情一無所知。

概述

幾乎每個人都聽說過 V8 引擎的概念,大多數人也都知道 JavaScript 是一門單執行緒語言或者知道它是基於回撥佇列的。

在這篇文章中,我們將詳細的介紹這些概念並且解釋 JavaScript 實際的執行方式,通過對這些細節的瞭解,你可以寫出更好、無阻塞的應用。

如果你是一名 JavaScript 新手,這篇文章將幫助你理解為什麼 JavaScript 和其它語言對比起來顯得那麼"奇怪"。

如果你是一名老司機,希望能夠為你帶來一些對 JavaScript 執行時的新思考。

JavaScript 引擎

說起 JavaScript 引擎,不得不提的就是 Google 的 V8 引擎,Chrome 和 Nodejs 內部也是使用的 V8。這裡有一個簡單的檢視:

simplified view for v8

引擎主要包含兩個元件:

  • Memory Heap: 記憶體分配發生的地方
  • Call Stack: 程式碼執行時棧幀的位置

執行時

幾乎所有的開發者都使用過瀏覽器中的 APIs (比如: setTimeout),然而,引擎並不提供這些 API。

那麼,這些 API 從何而來?

事實上,這是一個很複雜的問題。

simplified view for runtime

所以,除了引擎之外還有很多內容,包括我們呼叫的瀏覽器提供的 Web APIs,比如:DOM, AJAX, setTimeout 等。

然後,還有大名鼎鼎的事件迴圈和回撥佇列。

呼叫棧

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 01

對於呼叫棧中的每一個條目,我們叫做"棧幀"(Stack Frame)

這正是異常丟擲時堆疊追蹤的構造方式 - 基本上就是異常發生時呼叫棧的狀態。

我們看看如下程式碼:

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

function bar() {
    foo();
}

function start() {
    bar();
}

start();
複製程式碼

在瀏覽器執行(假設程式碼在 foo.js 檔案),可以在控制檯看到如下堆疊追蹤資訊:

call stack 02

"爆棧" - 當達到呼叫棧的最大大小的時候發生。而且這很容易發生,比如下面的這段牛逼的遞迴呼叫程式碼:

function foo() {
    foo();
}

foo();
複製程式碼

當引擎開始執行這段程式碼的時候,首先呼叫函式 "foo",但是這個函式接著遞迴的呼叫自己,並且沒有終止條件。相同的函式不斷的加到呼叫棧中,如下:

call stack 03

當呼叫棧中函式的數量超過其閥值的時候,瀏覽器決定動手了。瀏覽器會丟擲一個如下的異常資訊!

call stack 04

單執行緒上執行一個程式,對比在多執行緒環境下的執行簡單很多,因為不需要處理多執行緒執行下的一些複雜場景,比如:死鎖。

但是單執行緒也會很坑的,既然只有一個呼叫棧,那麼執行一個很慢很慢的計算的時候,你就會崩潰了。

併發與事件迴圈

如果你的呼叫棧中存在一個需要大量時間處理的函式的時候,會發生什麼?假如你想在瀏覽器端通過 JavaScript 進行復雜的影象轉換。

你可能會問 - 這也算是一個問題?問題就是當呼叫棧有函式在執行的時候,瀏覽器實際上不能做別的任何操作 - 它會被阻止。 這意味著瀏覽器不能渲染,不能執行別的程式碼,它被卡住了。如果你需要流暢的 UI 體驗,那就很糟糕了。

這還不是唯一的問題,一旦瀏覽器遇到很多很多的任務需要在呼叫棧中處理的時候,可能很長的一段時間內會停止響應。這個時候大多數瀏覽器就會採取行動,問你是否需要終止網頁。

event loop 01

這並不是最好的使用者體驗,是吧?

所以,我們如何處理繁重的程式碼而且不阻塞渲染或者不使瀏覽器停止響應呢,答案就是非同步回撥。

這將在本系列文章的第二部分詳細闡述。