JavaScript事件循環(huán)

解釋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í)行過程是簡單如下:

  1. 執(zhí)行g(shù)et方法和線程等待直到收到響應(yīng)
  2. 從google收到響應(yīng)然后返回給調(diào)用者儲存在一個變量中
  3. 變量的值(在這個例子中表示響應(yīng))輸出到控制臺
  4. "Done!"輸出到控制臺

讓我們在JavaScript使用Node的Request庫來做同樣的事:

request('http://www.google.com', function(error, response, body) {
  console.log(body);
});
console.log('Done!');

看起來只有一點(diǎn)不同禾嫉,但是行為是非常不同的

  1. 請求函數(shù)執(zhí)行,傳遞一個匿名函數(shù)作為回調(diào)函數(shù)蚊丐,當(dāng)未來某個時間點(diǎn)響應(yīng)是可用的時候執(zhí)行回調(diào)函數(shù)
  2. "Done!"立即輸出到控制臺
  3. 未來某個時間熙参,響應(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í)行

Paste_Image.png

這個回調(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)見過的事件模型

Paste_Image.png

首先贬堵,我們的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: Explained

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末栓撞,一起剝皮案震驚了整個濱河市遍膜,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瓤湘,老刑警劉巖瓢颅,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異弛说,居然都是意外死亡挽懦,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門木人,熙熙樓的掌柜王于貴愁眉苦臉地迎上來信柿,“玉大人,你說我怎么就攤上這事醒第∮嫒拢” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵淘讥,是天一觀的道長圃伶。 經(jīng)常有香客問我,道長蒲列,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任搀罢,我火速辦了婚禮蝗岖,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘榔至。我一直安慰自己抵赢,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布唧取。 她就那樣靜靜地躺著铅鲤,像睡著了一般。 火紅的嫁衣襯著肌膚如雪枫弟。 梳的紋絲不亂的頭發(fā)上邢享,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天,我揣著相機(jī)與錄音淡诗,去河邊找鬼骇塘。 笑死伊履,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的款违。 我是一名探鬼主播唐瀑,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼插爹!你這毒婦竟也來了哄辣?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤赠尾,失蹤者是張志新(化名)和其女友劉穎力穗,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體萍虽,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡睛廊,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了杉编。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片超全。...
    茶點(diǎn)故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖邓馒,靈堂內(nèi)的尸體忽然破棺而出嘶朱,到底是詐尸還是另有隱情,我是刑警寧澤光酣,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布疏遏,位于F島的核電站,受9級特大地震影響救军,放射性物質(zhì)發(fā)生泄漏财异。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一唱遭、第九天 我趴在偏房一處隱蔽的房頂上張望戳寸。 院中可真熱鬧,春花似錦拷泽、人聲如沸疫鹊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拆吆。三九已至,卻和暖如春脂矫,著一層夾襖步出監(jiān)牢的瞬間枣耀,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工羹唠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留奕枢,地道東北人娄昆。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像缝彬,于是被迫代替她去往敵國和親萌焰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容