解釋JavaScript的事件循環(huán)
這個帖子關(guān)于什么
瀏覽器普遍將JavaScript作為腳本語言常侦,這篇文章有利于你對JavaScript的事件驅(qū)動交互模型有一個基本的理解洋机,和JavaScript中的事件驅(qū)動交互模型與其他語言例如Ruby坠宴,Python,Java中傳統(tǒng)的請求-響應(yīng)模型有什么不同绷旗,在這個帖子中喜鼓,我會解釋一些JavaScript并發(fā)模型的核心概念,包括事件循環(huán)和消息隊(duì)列衔肢,希望能提高你對這門可能你寫了很久但是沒有完全理解的語言的理解
這個帖子為哪些人寫的
這個帖子的目標(biāo)是在客戶端或者服務(wù)端使用JavaScript開發(fā)的開發(fā)者庄岖,如果你已經(jīng)非常精通事件循環(huán),那么這篇文章你會感到很熟悉角骤,但是如果你不是這些人隅忿,我希望去提高你的基本理解,這樣你就能?更好理解你每天讀和寫的代碼
非阻塞I/O
在JavaScript中邦尊,幾乎所有的I/O是非阻塞的背桐,包括HTTP請求,數(shù)據(jù)庫操作和硬盤讀寫蝉揍;單線程操作要求運(yùn)行時只能執(zhí)行一個操作链峭,提供一個回調(diào)函數(shù)然后去做其他事,當(dāng)操作已經(jīng)完成又沾,一個消息伴隨著一個回調(diào)函數(shù)進(jìn)入消息隊(duì)列弊仪。在未來的某個時候,消息從隊(duì)列中移除并調(diào)用回調(diào)函數(shù)
然后開發(fā)者可能已經(jīng)熟悉這個交互模型捍掺,開發(fā)者已經(jīng)習(xí)慣于用戶交互界面的工作方式撼短,事件例如'mousedown','click'可以在任何時間觸發(fā)挺勿,它和同步的曲横,傳統(tǒng)服務(wù)端應(yīng)用運(yùn)用的請求-響應(yīng)模型是不同
讓我們比較兩段向www.google.com發(fā)起請求然后得到響應(yīng)在控制臺打印響應(yīng)的代碼,首先不瓶,Ruby使用Fataday:
response = Faraday.get 'http://www.google.com'
puts response
puts 'Done!'
這個執(zhí)行過程是簡單如下:
- 執(zhí)行g(shù)et方法和線程等待直到收到響應(yīng)
- 從google收到響應(yīng)然后返回給調(diào)用者儲存在一個變量中
- 變量的值(在這個例子中表示響應(yīng))輸出到控制臺
- "Done!"輸出到控制臺
讓我們在JavaScript使用Node的Request庫來做同樣的事:
request('http://www.google.com', function(error, response, body) {
console.log(body);
});
console.log('Done!');
看起來只有一點(diǎn)不同禾嫉,但是行為是非常不同的
- 請求函數(shù)執(zhí)行,傳遞一個匿名函數(shù)作為回調(diào)函數(shù)蚊丐,當(dāng)未來某個時間點(diǎn)響應(yīng)是可用的時候執(zhí)行回調(diào)函數(shù)
- "Done!"立即輸出到控制臺
- 未來某個時間熙参,響應(yīng)回來和執(zhí)行回調(diào)函數(shù),在控制臺輸出響應(yīng)body
事件循環(huán)
解耦調(diào)用者和響應(yīng)麦备,允許JavaScript運(yùn)行時在等待異步操作完成和觸發(fā)回調(diào)函數(shù)的時候可以做其他事情孽椰,但是回調(diào)函數(shù)在內(nèi)存中的哪里昭娩?回調(diào)函數(shù)的執(zhí)行順序?什么導(dǎo)致回調(diào)函數(shù)被執(zhí)行黍匾?
JavaScript運(yùn)行時包括一個消息隊(duì)列栏渺,消息隊(duì)列是一個儲存著被處理的消息和相關(guān)回調(diào)函數(shù)的列表。這些消息在外部事件被響應(yīng)的時候(例如一個鼠標(biāo)被點(diǎn)擊或者從一個HTTP請求收到響應(yīng)的時候)給予一個已經(jīng)提供的回調(diào)函數(shù)排進(jìn)消息隊(duì)列锐涯。設(shè)想一下磕诊,例如一個用戶點(diǎn)擊一個沒有提供回調(diào)函數(shù)的按鈕,那么沒有消息會被排進(jìn)消息隊(duì)列
在一個循環(huán)中纹腌,這個隊(duì)列查詢下一條消息(每次查詢表示為一個"tick")然后當(dāng)遇到一個消息的時候霎终,這個消息關(guān)聯(lián)的回調(diào)函數(shù)被執(zhí)行
這個回調(diào)函數(shù)調(diào)用充當(dāng)調(diào)用棧的初始幀,由于JavaScript是單線程的升薯,后續(xù)消息的查詢和處理被停止莱褒,等待調(diào)用棧中所有的調(diào)用返回。后來的(同步的)函數(shù)調(diào)用在調(diào)用棧中增加一個新的調(diào)用幀(例如覆劈,函數(shù)初始調(diào)用函數(shù)是changeColor)
function init() {
var link = document.getElementById("foo");
link.addEventListener("click", function changeColor() {
this.style.color = "burlywood";
});
}
init();
在這個例子中保礼,一個消息(和回調(diào)函數(shù),changeColor)在用戶點(diǎn)擊在'foo'元素上的時候和'onclick'事件觸發(fā)的時候被排進(jìn)隊(duì)列中责语。當(dāng)這個消息從隊(duì)列中排除的時候,他的回調(diào)函數(shù)changeColor被調(diào)用目派。當(dāng)changeColor返回(或拋出一個錯誤)的時候坤候,事件循環(huán)繼續(xù)執(zhí)行。只要函數(shù)changeColor存在企蹭,指定作為'foo'元素onclick的回調(diào)函數(shù)白筹,后續(xù)點(diǎn)擊在元素上將導(dǎo)致更多的消息(和關(guān)聯(lián)的回調(diào)函數(shù)changeColor)排進(jìn)隊(duì)列
排隊(duì)中額外的消息
如果一個函數(shù)調(diào)用在你的代碼中是異步的(例如setTimeout),這個提供的回調(diào)函數(shù)將作為一個不同的隊(duì)列消息的一部分最后執(zhí)行谅摄,在事件循環(huán)未來的一些tick中徒河,例如
function f() {
console.log("foo");
setTimeout(g, 0);
console.log("baz");
h();
}
function g() {
console.log("bar");
}
function h() {
console.log("blix");
}
f();
由于setTimeout非阻塞的特性,它的回調(diào)函數(shù)將至少0毫秒后在未來被執(zhí)行送漠,并且不是作為這個消息的一部分被處理顽照。在這個例子中,setTimeout是被調(diào)用闽寡, 傳遞一個回調(diào)函數(shù)g和0毫秒的延遲代兵。當(dāng)指定的時間到了(在這個例子中,幾乎馬上就到)爷狈,一個分離的消息包含回調(diào)函數(shù)g將被排進(jìn)消息隊(duì)列植影。控制臺活動的結(jié)果將看起來像這樣涎永,"foo"思币,"baz"鹿响,"blix"然后事件循環(huán)的下一個tick:"bar"。如果在同樣的調(diào)用幀setTimeout發(fā)起兩次調(diào)用----傳遞同樣的值作為第二個參數(shù)----他們的回調(diào)函數(shù)將按照調(diào)用順序排進(jìn)消息隊(duì)列
Web Workers
使用Web Workers讓你卸下分離執(zhí)行線程的昂貴操作谷饿,釋放主線程去做其他事抢野。Worker包括一個分離的消息隊(duì)列,事件循環(huán)和實(shí)例化的從原始線程分離的獨(dú)立的內(nèi)存空間各墨。Worker和主線程之間的通信通過消息傳遞指孤,消息傳遞看起來非常像傳統(tǒng)的,我們之前已經(jīng)見過的事件模型
首先贬堵,我們的Worker:
// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
pi = SomeLib.computePiToSpecifiedDecimals(e.data);
postMessage(pi);
};
onmessage = reportResult;
其次恃轩,HTML中Script標(biāo)簽中主要的代碼塊
// our main code, in a <script>-tag in our HTML page
var piWorker = new Worker("pi_calculator.js");
var logResult = function(e) {
console.log("PI: " + e.data);
};
piWorker.addEventListener("message", logResult, false);
piWorker.postMessage(100000);
在這個例子中,主線程產(chǎn)生一個Worker并且注冊logResult回調(diào)函數(shù)在"message"事件上黎做。在Worker中叉跛,reportResult函數(shù)注冊到它自己的"message"事件中。當(dāng)Worker線程收到來自主線程的消息蒸殿,worker將消息和相應(yīng)的回調(diào)函數(shù)排進(jìn)隊(duì)列筷厘。當(dāng)從隊(duì)列中排除的時候,一個消息被傳遞回主線程宏所,一個新的消息(伴隨著logResult回調(diào)函數(shù))被排進(jìn)隊(duì)列酥艳。使用這種方式開發(fā)者可以委托運(yùn)算密集行操作給分離的線程,釋放主線程繼續(xù)處理消息和處理事件
關(guān)于閉包的一些筆記
JavaScript的支持閉包允許你注冊回調(diào)函數(shù)爬骤,當(dāng)執(zhí)行時充石,有權(quán)訪問函數(shù)創(chuàng)建的環(huán)境,甚至回調(diào)函數(shù)的執(zhí)行創(chuàng)建一整個新的調(diào)用棧霞玄。這是特別有趣的知識骤铃,回調(diào)函數(shù)被調(diào)用作為不同消息的一部分而不是他們創(chuàng)建的那個消息,思考下面的例子:
function changeHeaderDeferred() {
var header = document.getElementById("header");
setTimeout(function changeHeader() {
header.style.color = "red";
return false;
}, 100);
return false;
}
changeHeaderDeferred();
在這個例子中坷剧,changeHeaderDeferred函數(shù)被執(zhí)行包括變量variable惰爬。函數(shù)setTimeout被調(diào)用,導(dǎo)致一個消息(加上changeHeader回調(diào)函數(shù))被添加到消息隊(duì)列大約100毫秒后惫企。changeHeaderDeferred函數(shù)然后返回false撕瞧,結(jié)束處理第一個消息----但是header變量仍然被通過閉包引用著,并且不會被垃圾回收雅任。當(dāng)?shù)诙€消息被處理(changeHeader函數(shù))风范,它有權(quán)訪問header變量在外部的函數(shù)作用域。一旦第二個消息(changeHeader函數(shù))被處理沪么,header變量將會被垃圾回收
順帶一說
JavaScript的事件驅(qū)動交互模型不同于已經(jīng)習(xí)慣的許多程序中的請求-響應(yīng)模型----但是就如你所見硼婿,它不是什么黑科技。使用一個簡單的消息隊(duì)列和事件循環(huán)禽车,JavaScript使開發(fā)者能圍繞著異步觸發(fā)的回調(diào)函數(shù)來構(gòu)建系統(tǒng)寇漫,當(dāng)?shù)却獠渴录l(fā)生的時候釋放運(yùn)行時去處理并發(fā)操作刊殉。然而,這僅僅是一種接近并發(fā)的方法州胳。在這篇文章的第二部分我將和那些已經(jīng)創(chuàng)建的在MRI Ruby(使用線程和GIL)记焊,EventMachine(Ruby),Java(線程)來比較JavaScript的并發(fā)性模型
額外閱讀
- The JavaScript Event Loop: Concurrency in the Language of the Web
- Concurrency model and Event Loop@MDN
- An intro to the Node.js platform, by Aaron Stannard
- Philip Roberts: What the heck is the event loop anyway? | JSConf EU 2014