隨著JavaScript(下文簡稱js)越來越流行,它在各個層面上都留下了身影:前端、後端、hybrid app、嵌入式裝置等。
這篇文章是這個系列中第一個深入挖掘js是如何工作的:我們認為理解了js的底層建築和執行方式可以使我們寫出更好的程式碼和應用。
總覽
應該很多人都聽過V8引擎這個概念,也知道js是一個單執行緒的語言,還有它使用了回撥佇列。
這篇文章裡我們會逐一解析每一個概念並解釋js是怎麼執行的。
如果你是js的初學者,那麼這篇文章會讓你瞭解為什麼js比起其他語言會那麼的“奇怪”。
如果你是js的高手,它會帶給你一些關於你每天使用的js執行時是怎樣工作的新穎知識。
JavaScript引擎
一個流行的js引擎是谷歌的V8引擎。它被使用在Chrome和Node.js中。這裡簡單地描述了他是什麼樣的:
引擎包含量兩大部分:
- 記憶體堆(Memory Heap)——記憶體分配的地方
- 呼叫棧——這是你的程式碼執行時棧幀的位置
執行時
在瀏覽器中有很多被幾乎每一位開發者使用的API(比如setTimeout
)。這些API,卻並不是引擎提供的。
所以,他們來自哪裡?
事實上要複雜一點點。
除了引擎以外還有一些東西。我們把這些瀏覽器提供的東西叫做Web API,比如DOM,AJAX,setTimeout等。
圖下方是大名鼎鼎的事件迴圈(event loop)和回撥佇列( callback queue)。
呼叫堆疊(Call Stack)
Js是一個單執行緒的語言。所以它也只有一個呼叫棧。這意味著它同時只能做一件事件。
呼叫棧是一個資料結構,基本上它記錄了我們的程式執行到哪了。如果我們執行進一個函式,那麼我們把它放在堆疊的頂部。如果我們從一個函式返回,那麼我們彈出(pop off)堆疊頂部的函式。這是堆疊所做的工作。
我們來看一個例子:
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
複製程式碼
當我們的引擎剛開始執行上述程式碼的時候,呼叫棧會是空。之後的步驟會如下圖所示:
呼叫棧中的每一條都稱作棧幀(Stack Frame)
這也解釋了當發生異常的時候堆疊軌跡( stack traces)是怎麼被建立起來的——其實就是異常發生時呼叫棧的狀態。看下面的列子:
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
複製程式碼
如果在Chrome中執行(假設執行的檔案叫foo.js),會產生下面的堆疊軌跡:
"Blowing the stack"——這個異常發生在你達到了呼叫棧最大值的時候。這個很容易出現,特別是在你不小心錯誤地使用了遞迴的時候:
function foo() {
foo();
}
foo();
複製程式碼
當引擎開始執行程式碼,我們會無止盡地執行這個函式。所以這個函式被不斷地堆在呼叫棧上面,就像這樣:
這時候瀏覽器會爆出:
程式碼執行在單執行緒的環境是一個很輕鬆的事,你不必擔心一些多執行緒帶來的複雜場景——比如,死鎖。
但是單執行緒也有一些限制。js只有一個呼叫棧,如何其中一些東西執行很慢怎麼辦?
併發 & 時間迴圈
當您在呼叫棧中行呼叫需要花費大量時間才能的函式時,會發生什麼情況?比如你想在瀏覽器中進行影象處理。
你可能會問——這為什麼會有問題?問題在於呼叫棧中有函式在執行,瀏覽器不能做其他的事情。這意味著瀏覽器不能渲染,不能跑其他程式碼。這會成為流暢UI介面的阻礙。
而且,一旦你的瀏覽器要處理太多的任務了,它會失去響應。一些瀏覽器會採取行動,詢問你是否終止這個網頁。
所以我們不卡死瀏覽器且擁有流暢UI的情況下執行大量程式碼呢?解決方案是非同步呼叫。
這會在本系列的下一篇文章中提到:“Inside the V8 engine + 5 tips on how to write optimized code”。(譯註:後續翻譯盡請關注)
原文連結:How JavaScript works: an overview of the engine, the runtime, and the call stack