說到JAVAScript的運行原理,自然繞不開JS引擎,運行上下文,單線程,事件循環,事件驅動,回調函數等概念。本文主要參考文章[1,2]。
為了更好的理解JavaScript如何工作的,首先要理解以下幾個概念。
- JS Engine(JS引擎)
- Runtime(運行上下文)
- Call Stack (調用棧)
- Event Loop(事件循環)
- Callback (回調)
1.JS Engine
簡單來說,JS引擎主要是對JS代碼進行詞法、語法等分析,通過編譯器將代碼編譯成可執行的機器碼讓計算機去執行。
目前最流行的JS引擎非V8莫屬了,Chrome瀏覽器和Node.js采用的引擎就是V8引擎。引擎的結構可以簡單由下圖表示:
就如JVM虛擬機一樣,JS引擎中也有堆(Memory Heap)和棧(Call Stack)的概念。
- 棧。用來存儲方法調用的地方,以及基礎數據類型(如var a = 1)也是存儲在棧里面的,會隨著方法調用結束而自動銷毀掉(入棧-->方法調用后-->出棧)。
- 堆。JS引擎中給對象分配的內存空間是放在堆中的。如var foo = {name: 'foo'} 那么這個foo所指向的對象是存儲在堆中的。
此外,JS中存在閉包的概念,對于基本類型變量如果存在與閉包當中,那么也將存儲在堆中。詳細可見此處1,3
關于閉包的情況,就涉及到Captured Variables。我們知道Local Variables是最簡單的情形,是直接存儲在棧中的。而Captured Variables是對于存在閉包情況和with,try catch情況的變量。
function foo () {
var x; // local variables
var y; // captured variable, bar中引用了y
function bar () {
// bar 中的context會capture變量y
use(y);
}
return bar;
}
復制代碼
如上述情況,變量y存在與bar()的閉包中,因此y是captured variable,是存儲在堆中的。
2.RunTime
JS在瀏覽器中可以調用瀏覽器提供的API,如window對象,DOM相關API等。這些接口并不是由V8引擎提供的,是存在與瀏覽器當中的。因此簡單來說,對于這些相關的外部接口,可以在運行時供JS調用,以及JS的事件循環(Event Loop)和事件隊列(Callback Queue),把這些稱為RunTime。有些地方也把JS所用到的core lib核心庫也看作RunTime的一部分。
同樣,在Node.js中,可以把Node的各種庫提供的API稱為RunTime。所以可以這么理解,Chrome和Node.js都采用相同的V8引擎,但擁有不同的運行環境(RunTime Environments)[4]。
3.Call Stack
JS被設計為單線程運行的,這是因為JS主要用來實現很多交互相關的操作,如DOM相關操作,如果是多線程會造成復雜的同步問題。因此JS自誕生以來就是單線程的,而且主線程都是用來進行界面相關的渲染操作 (為什么說是主線程,因為html5 提供了Web Worker,獨立的一個后臺JS,用來處理一些耗時數據操作。因為不會修改相關DOM及頁面元素,因此不影響頁面性能),如果有阻塞產生會導致瀏覽器卡死。
如果一個遞歸調用沒有終止條件,是一個死循環的話,會導致調用棧內存不夠而溢出,如:
function foo() {
foo();
}
foo();
復制代碼
例子中foo函數循環調用其本身,且沒有終止條件,瀏覽器控制臺輸出調用棧達到最大調用次數。
JS線程如果遇到比較耗時操作,如讀取文件,AJAX請求操作怎么辦?這里JS用到了Callback回調函數來處理。
對于Call%20Stack中的每個方法調用,都會形成它自己的一個執行上下文Execution%20Context,關于執行上下文的詳細闡述請看這篇文章
4.Event%20Loop%20&%20Callback
JS通過回調的方式,異步處理耗時的任務。一個簡單的例子:
var%20result%20=%20ajax('...');
console.log(result);
復制代碼
此時并不會得到result的值,result是undefined。這是因為ajax的調用是異步的,當前線程并不會等到ajax請求到結果后才執行console.log語句。而是調用ajax后請求的操作交給回調函數,自己是立刻返回。正確的寫法應該是:
ajax('...',%20function(result)%20{
%20console.log(result);
})
復制代碼
此時才能正確輸出請求返回的結果。
JS引擎其實并不提供異步的支持,異步支持主要依賴于運行環境(瀏覽器或Node.js)。
So,%20for%20example,%20when%20your%20JavaScript%20program%20makes%20an%20Ajax%20request%20to%20fetch%20some%20data%20from%20the%20server,%20you%20set%20up%20the%20“response”%20code%20in%20a%20function%20(the%20“callback”),%20and%20the%20JS%20Engine%20tells%20the%20hosting%20environment:%20“Hey,%20I’m%20going%20to%20suspend%20execution%20for%20now,%20but%20whenever%20you%20finish%20with%20that%20network%20request,%20and%20you%20have%20some%20data,%20please%20call%20this%20function%20back.”
The%20browser%20is%20then%20set%20up%20to%20listen%20for%20the%20response%20from%20the%20network,%20and%20when%20it%20has%20something%20to%20return%20to%20you,%20it%20will%20schedule%20the%20callback%20function%20to%20be%20executed%20by%20inserting%20it%20into%20the%20event%20loop.
上面這兩段話摘自于How%20JavaScript%20works,以通俗的方式解釋了JS如何調用回調函數實現異步處理。
所以什么是Event%20Loop?
Event%20Loop只做一件事情,負責監聽Call%20Stack和Callback%20Queue。當Call%20Stack里面的調用棧運行完變成空了,Event%20Loop就把Callback%20Queue里面的第一條事件(其實就是回調函數)放到調用棧中并執行它,后續不斷循環執行這個操作。
一個setTimeout的例子以及對應的Event%20Loop動態圖:
console.log('Hi');
setTimeout(function%20cb1()%20{%20
%20console.log('cb1');
},%205000);
console.log('Bye');
復制代碼
setTimeout有個要注意的地方,如上述例子延遲5s執行,不是嚴格意義上的5s,正確來說是至少5s以后會執行。因為Web API會設定一個5s的定時器,時間到期后將回調函數加到隊列中,此時該回調函數還不一定會馬上運行,因為隊列中可能還有之前加入的其他回調函數,而且還必須等到Call Stack空了之后才會從隊列中取一個回調執行。
所以常見的setTimeout(callback, 0) 的做法就是為了在常規的調用介紹后馬上運行回調函數。
console.log('Hi');
setTimeout(function() {
console.log('callback');
}, 0);
console.log('Bye');
// 輸出
// Hi
// Bye
// callback
復制代碼
在說一個容易犯錯的栗子:
for (var i = 0; i < 5; i++) {
setTimeout(function() {
console.log(i);
}, 1000 * i);
}
// 輸出:5 5 5 5 5
復制代碼
上面這個栗子并不是輸出0,1,2,3,4,第一反應覺得應該是這樣。但梳理了JS的時間循環后,應該很容易明白。
調用棧先執行 for(var i = 0; i < 5; i++) {...}方法,里面的定時器會到時間后會直接把回調函數放到事件隊列中,等for循環執行完在依次取出放進調用棧。當for循環執行完時,i的值已經變成5,所以最后輸出全都是5。
關于定時器又可以看看這篇有意思的文章
最后關于Event Loop,可以參考下這個視頻。到目前為止說的event loop是前端瀏覽器中的event loop,關于Nodejs的Event Loop的細節闡述,請看我的另一篇文章Node.js design pattern : Reactor (Event Loop)。兩者的區別對比可查看這篇文章你不知道的Event Loop,對兩種event loop做了相關總結和比較。
總結
最后總結一下,JS的運行原理主要有以下幾個方面:
- JS引擎主要負責把JS代碼轉為機器能執行的機器碼,而JS代碼中調用的一些WEB API則由其運行環境提供,這里指的是瀏覽器。
- JS是單線程運行,每次都從調用棧出取出代碼進行調用。如果當前代碼非常耗時,則會阻塞當前線程導致瀏覽器卡頓。
- 回調函數是通過加入到事件隊列中,等待Event Loop拿出并放到調用棧中進行調用。只有Event Loop監聽到調用棧為空時,才會從事件隊列中從隊頭拿出回調函數放進調用棧里。






