說(shuo)到JavaScript的運行原(yuan)理,自(zi)然(ran)繞不開JS引擎,運行上下文,單線程,事(shi)件循環,事(shi)件驅動,回調函數等概(gai)念
為了更好(hao)的(de)(de)理解(jie)JavaScript如何工作的(de)(de),首先要理解(jie)以下(xia)幾個概(gai)念。
- JS Engine(JS引擎)
- Runtime(運行上下文)
- Call Stack (調用棧)
- Event Loop(事件循環)
- Callback (回調)
1.JS Engine
簡單來(lai)說,JS引擎主要(yao)是(shi)對JS代(dai)碼進(jin)行(xing)詞法(fa)、語(yu)法(fa)等(deng)分析,通過編(bian)(bian)譯(yi)器將代(dai)碼編(bian)(bian)譯(yi)成(cheng)可執行(xing)的機器碼讓計算機去執行(xing)。
目前最流行的JS引擎非V8莫屬了(le),Chrome瀏覽(lan)器和Node.js采用的引擎就是(shi)V8引擎。
就(jiu)如JVM虛擬機(ji)一樣,JS引擎中也有堆(Memory Heap)和棧(Call Stack)的概念。
-
棧。用(yong)來存(cun)儲方(fang)法(fa)調用(yong)的地方(fang),以及(ji)基礎數據類(lei)型(如var a = 1)也是存(cun)儲在棧里(li)面的,會隨著方(fang)法(fa)調用(yong)結束(shu)而(er)自(zi)動銷毀掉(入棧-->方(fang)法(fa)調用(yong)后-->出棧)。
-
堆(dui)。JS引擎(qing)中給對(dui)象分配的(de)內(nei)存空間是放在堆(dui)中的(de)。如var foo = {name: 'foo'} 那么(me)這個foo所(suo)指向的(de)對(dui)象是存儲在堆(dui)中的(de)。
此外,JS中存(cun)在(zai)閉包的概(gai)念,對于基本類型變量如果存(cun)在(zai)與閉包當(dang)中,那(nei)么也將存(cun)儲(chu)在(zai)堆中。
2.RunTime
JS在(zai)(zai)瀏(liu)覽器中可(ke)以(yi)調用瀏(liu)覽器提(ti)供的(de)API,如window對象,DOM相(xiang)關API等(deng)。這些接口并不是由(you)V8引擎(qing)提(ti)供的(de),是存在(zai)(zai)與瀏(liu)覽器當中的(de)。因此簡單來說,對于(yu)這些相(xiang)關的(de)外部接口,可(ke)以(yi)在(zai)(zai)運行時(shi)供JS調用,以(yi)及JS的(de)事(shi)件循環(Event Loop)和事(shi)件隊(dui)列(Callback Queue),把這些稱為RunTime。有些地方也把JS所(suo)用到(dao)的(de)core lib核心庫也看(kan)作RunTime的(de)一部分(fen)。
同(tong)樣,在(zai)Node.js中(zhong),可以把Node的各種庫提(ti)供的API稱(cheng)為(wei)RunTime。所以可以這么理解,Chrome和Node.js都采用(yong)相同(tong)的V8引擎,但擁(yong)有不(bu)同(tong)的運行環境(RunTime Environments)。
3.Call Stack
JS被設計為(wei)單線(xian)程運(yun)行(xing)的(de),這是因(yin)為(wei)JS主要用(yong)來(lai)實(shi)現(xian)很多(duo)交(jiao)互相關的(de)操作,如(ru)DOM相關操作,如(ru)果(guo)是多(duo)線(xian)程會(hui)(hui)造成(cheng)復雜(za)的(de)同(tong)步問題。因(yin)此JS自誕(dan)生(sheng)以來(lai)就是單線(xian)程的(de),而且主線(xian)程都(dou)是用(yong)來(lai)進行(xing)界(jie)面(mian)相關的(de)渲染(ran)操作**(為(wei)什么(me)說是主線(xian)程,因(yin)為(wei)HTML5 提供了Web Worker,獨立(li)的(de)一個后臺(tai)JS,用(yong)來(lai)處理一些耗時數據操作。因(yin)為(wei)不會(hui)(hui)修改相關DOM及頁(ye)面(mian)元素,因(yin)此不影響頁(ye)面(mian)性(xing)能)**,如(ru)果(guo)有(you)阻塞產生(sheng)會(hui)(hui)導致瀏覽器卡死。
如(ru)果一(yi)個遞(di)歸調用沒(mei)有終止(zhi)條件,是(shi)一(yi)個死循(xun)環(huan)的話,會導致調用棧(zhan)內存不夠(gou)而(er)溢出,如(ru):
function foo() {
foo();
}
foo();
例(li)子中foo函(han)數(shu)循環調(diao)用其本身(shen),且沒(mei)有終止條件,瀏(liu)覽器控(kong)制臺輸(shu)出(chu)調(diao)用棧達到(dao)最大(da)調(diao)用次數(shu)。
JS線(xian)程如果遇(yu)到比較耗時操作,如讀取文(wen)件(jian),AJAX請求操作怎(zen)么辦(ban)?這里JS用到了Callback回(hui)調函(han)數來處理。
4.Event Loop & Callback
JS通過(guo)回調的方式,異步處(chu)理耗時的任務。一個簡單的例(li)子:
var result = ajax('...');
console.log(result);
此時并不(bu)會得到result的值,result是(shi)undefined。這是(shi)因為ajax的調(diao)用(yong)是(shi)異步的,當前線程并不(bu)會等(deng)到ajax請求(qiu)到結果后才執行console.log語(yu)句。而是(shi)調(diao)用(yong)ajax后請求(qiu)的操作交(jiao)給回調(diao)函數(shu),自己是(shi)立刻返回。正確的寫法(fa)應(ying)該(gai)是(shi):
ajax('...', function(result) {
console.log(result);
})
此時才能正確輸出請求返回的結(jie)果。
JS引(yin)擎其實并不提供異(yi)步(bu)的支持,異(yi)步(bu)支持主要(yao)依賴于運(yun)行環境(瀏(liu)覽器或Node.js)。
So, for example, when your JavaScript program makes an Ajax request to fetch some data from the server, you set up the “response” code in a function (the “callback”), and the JS Engine tells the hosting environment:
“Hey, I’m going to suspend execution for now, but whenever you finish with that network request, and you have some data, please call this function back.”
The browser is then set up to listen for the response from the network, and when it has something to return to you, it will schedule the callback function to be executed by inserting it into the event loop.
上面這兩段話摘(zhai)自于How JavaScript works,以通俗(su)的方(fang)式解釋(shi)了JS如何調用回調函數實現(xian)異步處(chu)理。
所以什么是Event Loop?
Event Loop只做一件事情,負責監聽Call Stack和Callback Queue。當Call Stack里面的調用棧運行完變成空了,Event Loop就把Callback Queue里面的第一條事件(其實就是回調函數)放到調用棧中并執行它,這個過程就是Event Loop的一個tick,后續(xu)不斷(duan)循環執行這個操作(zuo)。
一個setTimeout的例子(zi):
console.log('Hi');
setTimeout(function cb1() {
console.log('cb1');
}, 5000);
console.log('Bye');
setTimeout有(you)個(ge)要注意的(de)地方,如(ru)上(shang)述例子延(yan)遲5s執行,不(bu)是(shi)嚴格(ge)意義(yi)上(shang)的(de)5s,正確來說(shuo)是(shi)至少5s以后(hou)會(hui)執行。因(yin)為Web API會(hui)設定一個(ge)5s的(de)定時(shi)器(qi),時(shi)間到期后(hou)將回調函數(shu)加到隊(dui)列(lie)中,此時(shi)該(gai)回調函數(shu)還(huan)不(bu)一定會(hui)馬上(shang)運行,因(yin)為隊(dui)列(lie)中可能還(huan)有(you)之前加入的(de)其(qi)他回調函數(shu),而且還(huan)必須(xu)等到Call Stack空了(le)之后(hou)才會(hui)從隊(dui)列(lie)中取一個(ge)回調執行。
所以常見的setTimeout(callback, 0) 的做法就是(shi)為(wei)了在(zai)常規的調(diao)(diao)用介紹后馬上運行(xing)回調(diao)(diao)函數(shu)。
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
上面這(zhe)個栗子并不是輸出(chu)0,1,2,3,4,第一(yi)反應覺得應該是這(zhe)樣。但梳(shu)理了JS的時間循環后,應該很(hen)容易(yi)明白。
調用棧先執行 for(var i = 0; i < 5; i++) {...}方法,里面的定時器會到時間后會直接把回調函數放到事件隊列中,等for循環執行完在依次取出放進調用棧。當for循環執行完時,i的值(zhi)已經變成5,所以最后輸出全都是5。
總結
最后總結(jie)一下,JS的(de)運行(xing)原理主要有(you)以(yi)下幾個方面:
-
JS引擎主要負責把JS代(dai)碼(ma)轉為(wei)機(ji)器能(neng)執行的(de)機(ji)器碼(ma),而JS代(dai)碼(ma)中調用(yong)的(de)一些WEB API則由其(qi)運行環境提供,這里指的(de)是瀏覽(lan)器。
-
JS是單線程(cheng)(cheng)運行,每次都從調(diao)用棧出(chu)(chu)取出(chu)(chu)代碼(ma)進行調(diao)用。如(ru)果(guo)當(dang)前代碼(ma)非常(chang)耗時,則會阻塞當(dang)前線程(cheng)(cheng)導(dao)致瀏覽(lan)器卡頓。
-
回調(diao)函數是通(tong)過加入到(dao)事(shi)件隊列中(zhong),等(deng)待(dai)Event Loop拿出并放到(dao)調(diao)用(yong)棧(zhan)中(zhong)進行調(diao)用(yong)。只(zhi)有Event Loop監(jian)聽到(dao)調(diao)用(yong)棧(zhan)為空(kong)時(shi),才會從(cong)(cong)事(shi)件隊列中(zhong)從(cong)(cong)隊頭(tou)拿出回調(diao)函數放進調(diao)用(yong)棧(zhan)里。