概覽
幾乎所有人都已經聽說了V8引擎的概念,大多數人都知道JavaScript是單執行緒執行的或者說是使用回撥佇列的。
接下來,我們將詳細的講述這些概念,解釋JavaScript到底是怎樣執行的。當知道了這些細節後,你就能合理利用已有的API寫出更好的,非阻塞的應用。 如果你是JavaScript新手,這篇部落格可以幫助你理解為什麼相對於其他語言,JavaScript顯得如此奇怪。
如果你是比較有經驗的JavaScript開發者,希望這篇部落格可以讓你對你每天使用的JavaScript執行時到底是怎樣執行的有一些新的見解。
JavaScript引擎
一個流行的JavaScript引擎是谷歌的V8引擎。例如在Chrome和Node.js中使用的就是V8引擎。下圖是V8引擎一個非常簡單的預覽:
V8引擎由兩個主要元件所組成:
-
Memory Heap--記憶體分配區
-
Call Stack--程式碼執行時棧
執行時
大部分JavaScript開發者都使用過瀏覽器的API(例如“setTimeout”)。然而這些API都不是由引擎提供的。 那麼,它們來自哪裡呢? 真實情況有點複雜。
所以除了引擎還有喝很多其他的東西。有瀏覽器提供的Web API,像DOM,AJAX,setTimeout等等。 然後還有非常有名的event loop和call queue。
呼叫棧
JavaScript是一門單執行緒的程式語言,也就是說它只有一個呼叫棧,因此它只能一次做一件事。
呼叫棧是一個記錄程式執行到哪裡的資料結構。呼叫函式的時候,我們會把它放到棧的最頂部。從函式返回的時候,我們會把它從棧的最頂部彈出來。這就是呼叫棧做的所有的事情。
我們來看一個例子,看一下如下程式碼:
function multiply(x, y) {
return x * y;
}
function printSquare(x) {
var s = multiply(x, x);
console.log(s);
}
printSquare(5);
複製程式碼
當引擎開始執行這段程式碼的時候,呼叫棧是空的。接下來,每一步如下所示:
每次進入呼叫棧成為棧楨。 這就是當一個異常丟擲時,棧的記錄是怎樣組成的,基本上就是當一個異常發生的時候呼叫棧的狀態。看一下如下程式碼:
function foo() {
throw new Error('SessionStack will help you resolve crashes :)');
}
function bar() {
foo();
}
function start() {
bar();
}
start();
複製程式碼
如果是執行在Chrome中(假定這段程式碼在foo.js檔案中),將會生成如下棧記錄:
“棧溢位”--這個發生在超過呼叫棧最大空間的時後。這非常容易發生,特別是當你使用遞迴但又沒有非常嚴格的測試你的程式碼的時候。看一下如下程式碼示例:
function foo() {
foo();
}
foo();
複製程式碼
當引擎開始執行這段程式碼的時候,首先呼叫“foo”函式,但是這個函式是遞迴的,開始呼叫自己並且沒有結束條件。所以每一步執行,相同的函式都會一遍又一遍的加入到呼叫棧中,看上去就像這樣:
然而在某個時間點上呼叫棧中的函式呼叫數量將會超過呼叫棧的實際大小,此時瀏覽器決定採取行動,丟擲一個錯誤,我們就會看到像下面這樣的提示:
在單執行緒上執行程式碼是非常容易的,你不用處理在多執行緒中發生的複雜的場景--例如死鎖。
Concurrency & the Event Loop
在呼叫棧中存在需要花費很多時間的函式呼叫時會發生什麼呢?例如,想象一下你需要在瀏覽器中利用JavaScript來做一些複雜的圖片轉換。
你可能會問--這有什麼好問的?問題就是呼叫棧在執行函式的時候,瀏覽器不能做其他的事--瀏覽器被阻塞了。這意味著瀏覽器將不能渲染,不能執行其他程式碼,就是說被阻塞了。如果你想要一個體驗很好,執行流暢的應用,這將會是很大的問題。
而且還不止這一個問題。一旦你的瀏覽器在呼叫棧中處理很多工,它將會在很長時間內得不到響應,大多數瀏覽器將會丟擲一個錯誤來採取行動,詢問你是否要結束這個web頁面。
這不是最好的使用者體驗,不是嗎?
所以,我們怎樣才能在執行很重的程式碼的時候,不阻塞UI,使瀏覽器不需要等待響應呢?解決方案就是非同步回撥。
我們將會在下一節詳細講述。
對V8引擎的內部機制感興趣的同學可以看這裡。