JavaScript中的事件循環(huán)與消息隊(duì)列

2019-4-01更新:采納JSC引擎的術(shù)語(yǔ),我們把宿主(瀏覽器潮剪、Node環(huán)境)發(fā)起的任務(wù)稱(chēng)為宏任務(wù)(如SetTimeout)锭碳,把JavaScript引擎發(fā)起的任務(wù)稱(chēng)為微觀任務(wù)(如Promise)袁稽。


我們?cè)诮佑|到JavaScript語(yǔ)言的時(shí)候就經(jīng)常聽(tīng)到別人介紹JavaScript 是單線程、異步擒抛、非阻塞推汽、解釋型腳本語(yǔ)言。
究竟應(yīng)該如何理解這句話呢歧沪?

確切的說(shuō)歹撒,對(duì)于開(kāi)發(fā)者的開(kāi)發(fā)過(guò)程來(lái)說(shuō),js確實(shí)只有一個(gè)線程(由JS引擎維護(hù))诊胞,這個(gè)線程用來(lái)負(fù)責(zé)解釋和執(zhí)行JavaScript代碼暖夭,我們可以稱(chēng)其為主線程。例如在控制臺(tái)輸入如下代碼:

console.log("a");
console.log("b");
console.log("c");
//依次輸出a,b,c

可以看出,這段代碼在主線程上是按照順序執(zhí)行的迈着。但是我們平時(shí)的任務(wù)處理可能并不會(huì)直接獲取到結(jié)果竭望,這種情況下如果仍然使用同步方法,例如發(fā)起一個(gè)ajax請(qǐng)求裕菠,大概500ms后受到響應(yīng)咬清,在這個(gè)過(guò)程中,后面的任務(wù)就會(huì)被阻塞奴潘,瀏覽器頁(yè)面就會(huì)阻塞所有用戶(hù)交互旧烧,呈“卡死”狀態(tài)。

console.log("a");
$.ajax({
  url:"xxx",
  async:false, //同步請(qǐng)求ajax
  success:function(){
    console.log("b");
  }
})
console.log("c");

這種同步的方式對(duì)于用戶(hù)操作非常不友好萤彩,所以大部分耗時(shí)的任務(wù)在JS中都會(huì)通過(guò)異步的方式實(shí)現(xiàn)粪滤。雖然js引擎只維護(hù)一個(gè)主線程用來(lái)解釋執(zhí)行JS代碼,但實(shí)際上瀏覽器環(huán)境中還存在其他的線程雀扶,例如處理AJAX,DOM肆汹,定時(shí)器等愚墓,我們可以稱(chēng)他們?yōu)楣ぷ骶€程。同時(shí)瀏覽器中還維護(hù)了一個(gè)消息隊(duì)列昂勉,主線程會(huì)將執(zhí)行過(guò)程中遇到的異步請(qǐng)求發(fā)送給這個(gè)消息隊(duì)列浪册,等到主線程空閑時(shí)再來(lái)執(zhí)行消息隊(duì)列中的任務(wù)。
同步任務(wù)的缺點(diǎn)是阻塞岗照,異步任務(wù)的缺點(diǎn)是會(huì)使代碼執(zhí)行順序難以判斷村象。
兩者比較一下我們還是更傾向于后者。
到目前為止攒至,我們已經(jīng)涉及到了幾個(gè)名詞厚者,主線程,js引擎迫吐,事件循環(huán)库菲,消息隊(duì)列等。接下來(lái)會(huì)對(duì)這些名詞一一進(jìn)行解釋志膀。

js引擎

我們所熟悉的引擎是chrome瀏覽器中和node.js中使用的V8引擎熙宇。它的大致組成如圖:


v8引擎

這個(gè)引擎主要由兩個(gè)部分組成,內(nèi)存堆和調(diào)用棧溉浙。(只負(fù)責(zé)取消息烫止,不負(fù)責(zé)生產(chǎn)消息)
內(nèi)存堆:進(jìn)行內(nèi)存分配。如變量賦值戳稽。
調(diào)用棧:這是代碼在棧幀中執(zhí)行的地方馆蠕。調(diào)用棧中順序執(zhí)行主線程的代碼,當(dāng)調(diào)用棧中為空時(shí),js引擎會(huì)去消息隊(duì)列取消息荆几。取到后就執(zhí)行吓妆。JavaScript是單線程的編程語(yǔ)言,意味著它有一個(gè)單一的調(diào)用棧吨铸。因此它只能在同一時(shí)間做一件事情行拢。調(diào)用棧是一種數(shù)據(jù)結(jié)構(gòu),它基本上記錄了我們?cè)诔绦蛑械氖裁次恢玫āH绻覀儾饺胍粋€(gè)函數(shù)中舟奠,我們會(huì)把這些數(shù)據(jù)放在堆棧的頂部。如果我們從一個(gè)函數(shù)中返回房维,這些數(shù)據(jù)將會(huì)從棧頂彈出沼瘫。這就是堆棧的用途。調(diào)用棧中的每個(gè)條目叫做棧幀咙俩。當(dāng)我們?cè)赾hrome調(diào)試窗口中看到拋出的錯(cuò)誤時(shí)耿戚,就能夠看到大致的調(diào)用順序。


js運(yùn)行時(shí)

運(yùn)行時(shí)

我們經(jīng)常使用的一些API并不是js引擎中提供的阿趁,例如setTimeout膜蛔。它們其實(shí)是在瀏覽器中提供的,也就是運(yùn)行時(shí)提供的脖阵,因此皂股,實(shí)際上除了JavaScript引擎以外,還有其他的組件命黔。其中有個(gè)組件就是由瀏覽器提供的呜呐,叫Web APIs,像DOM悍募,AJAX蘑辑,setTimeout等等。
然后還有就是非常受歡迎的事件循環(huán)和回調(diào)隊(duì)列搜立。
運(yùn)行時(shí)負(fù)責(zé)給引擎線程發(fā)送消息以躯,只負(fù)責(zé)生產(chǎn)消息,不負(fù)責(zé)取消息啄踊。

消息隊(duì)列和事件循環(huán)

主線程在執(zhí)行過(guò)程中遇到了異步任務(wù)忧设,就發(fā)起函數(shù)或者稱(chēng)為注冊(cè)函數(shù),通過(guò)event loop線程通知相應(yīng)的工作線程(如ajax颠通,dom址晕,setTimout等),同時(shí)主線程繼續(xù)向后執(zhí)行顿锰,不會(huì)等待谨垃。等到工作線程完成了任務(wù)启搂,eventloop線程會(huì)將消息添加到消息隊(duì)列中,如果此時(shí)主線程上調(diào)用棧為空就執(zhí)行消息隊(duì)列中排在最前面的消息刘陶,依次執(zhí)行胳赌。
新的消息進(jìn)入隊(duì)列的時(shí)候,會(huì)自動(dòng)排在隊(duì)列的尾端匙隔。
單線程意味著js任務(wù)需要排隊(duì)疑苫,如果前一個(gè)任務(wù)出現(xiàn)大量的耗時(shí)操作,后面的任務(wù)得不到執(zhí)行纷责,任務(wù)的積累會(huì)導(dǎo)致頁(yè)面的“假死”捍掺。這也是js編程一直在強(qiáng)調(diào)需要回避的“坑”。
主線程會(huì)循環(huán)上述步驟再膳,事件循環(huán)就是主線程重復(fù)從消息隊(duì)列中取消息挺勿、執(zhí)行的過(guò)程。
需要注意的是 GUI渲染線程與JS引擎是互斥的喂柒,當(dāng)JS引擎執(zhí)行時(shí)GUI線程會(huì)被掛起不瓶,GUI更新會(huì)被保存在一個(gè)隊(duì)列中等到JS引擎空閑時(shí)立即被執(zhí)行。因此頁(yè)面渲染都是在js引擎主線程調(diào)用棧為空時(shí)進(jìn)行的灾杰。

其實(shí) 事件循環(huán) 機(jī)制和 消息隊(duì)列 的維護(hù)是由事件觸發(fā)線程控制的湃番。

事件觸發(fā)線程 同樣是瀏覽器渲染引擎提供的,它會(huì)維護(hù)一個(gè) 消息隊(duì)列吭露。

JS引擎線程遇到異步(DOM事件監(jiān)聽(tīng)、網(wǎng)絡(luò)請(qǐng)求尊惰、setTimeout計(jì)時(shí)器等...)讲竿,會(huì)交給相應(yīng)的線程單獨(dú)去維護(hù)異步任務(wù),等待某個(gè)時(shí)機(jī)(計(jì)時(shí)器結(jié)束弄屡、網(wǎng)絡(luò)請(qǐng)求成功题禀、用戶(hù)點(diǎn)擊DOM),然后由 事件觸發(fā)線程 將異步對(duì)應(yīng)的 回調(diào)函數(shù) 加入到消息隊(duì)列中膀捷,消息隊(duì)列中的回調(diào)函數(shù)等待被執(zhí)行迈嘹。

同時(shí),JS引擎線程會(huì)維護(hù)一個(gè) 執(zhí)行棧全庸,同步代碼會(huì)依次加入執(zhí)行棧然后執(zhí)行秀仲,結(jié)束會(huì)退出執(zhí)行棧。
如果執(zhí)行棧里的任務(wù)執(zhí)行完成壶笼,即執(zhí)行棧為空的時(shí)候(即JS引擎線程空閑)神僵,事件觸發(fā)線程才會(huì)從消息隊(duì)列取出一個(gè)任務(wù)(即異步的回調(diào)函數(shù))放入執(zhí)行棧中執(zhí)行。

執(zhí)行順序

了解了事件循環(huán)和消息隊(duì)列之后覆劈,接下來(lái)就是弄清楚當(dāng)同步任務(wù)和異步任務(wù)都存在時(shí)保礼,代碼執(zhí)行的順序究竟是怎么樣的沛励。
舉個(gè)例子:

console.log("a");
setTimeout(function(){
  console.log("b")},1000
);
console.log("c");

相信所有人都知道執(zhí)行順序是 a, c , b。
如果變化一下:

console.log("a");
setTimeout(function(){
  console.log("b")},0
);
console.log("c");

相信通過(guò)上面的內(nèi)容炮障,大部分人也都知道執(zhí)行順序還是a,c,b目派。setTimeout在主線程執(zhí)行時(shí)被添加到了消息隊(duì)列中,等待主線程調(diào)用棧為空時(shí)胁赢,再?gòu)南㈥?duì)列中取出執(zhí)行企蹭。因此setTimeout中的延時(shí)時(shí)間并非確切的執(zhí)行時(shí)間,實(shí)際上應(yīng)該理解為添加到消息隊(duì)列中的延遲時(shí)間徘键。以上述代碼為例练对,如果console.log("c")處是一個(gè)計(jì)算量很大的任務(wù),或者消息隊(duì)列中已經(jīng)存在了若干個(gè)等待處理的消息吹害。setTimeout都將延遲都將大于設(shè)置的延遲時(shí)間螟凭。

ES6 Promise

以上的內(nèi)容在ES6之前就基本cover了執(zhí)行順序的問(wèn)題,但是在ES6引入了promise后它呀,產(chǎn)生了一個(gè)新的名詞”微任務(wù)(microtask)“螺男。微任務(wù)的執(zhí)行順序與之前我們所說(shuō)的任務(wù)(我們可以稱(chēng)之為”宏任務(wù)“)是不同的。

console.log('script start')

setTimeout(function() {
    console.log('timer over')
}, 0)

Promise.resolve().then(function() {
    console.log('promise1')
}).then(function() {
    console.log('promise2')
})

console.log('script end')

輸出的結(jié)果是:
script start
script end
promise1
promise2
timer over
你答對(duì)了嗎纵穿?
我猜這里讓你困惑的一定是為什么promise1和promise2在timer over之前輸出了下隧。下面我們來(lái)解釋一下微任務(wù)這個(gè)概念。

  • 一個(gè)線程中谓媒,事件循環(huán)是唯一的淆院,但是任務(wù)隊(duì)列可以擁有多個(gè)。
  • 任務(wù)隊(duì)列又分為macro-task(宏任務(wù))與micro-task(微任務(wù))句惯,在最新標(biāo)準(zhǔn)中土辩,它們被分別稱(chēng)為task與jobs。
  • macro-task大概包括:script(整體代碼), setTimeout, setInterval, setImmediate, I/O, UI rendering抢野。
  • micro-task大概包括: process.nextTick, Promise, Object.observe(已廢棄), MutationObserver(H5新特性)
  • setTimeout/Promise等我們稱(chēng)之為任務(wù)源拷淘。而進(jìn)入任務(wù)隊(duì)列的是他們指定的具體執(zhí)行任務(wù)。
  • 來(lái)自不同任務(wù)源的任務(wù)會(huì)進(jìn)入到不同的任務(wù)隊(duì)列指孤。其中setTimeout與setInterval是同源的启涯。
  • 事件循環(huán)的順序,決定了JavaScript代碼的執(zhí)行順序恃轩。它從script(整體代碼)開(kāi)始第一次循環(huán)结洼。之后全局上下文進(jìn)入函數(shù)調(diào)用棧。直到調(diào)用棧清空(只剩全局)详恼,然后執(zhí)行所有的micro-task补君。當(dāng)所有可執(zhí)行的micro-task執(zhí)行完畢之后。循環(huán)再次從macro-task開(kāi)始昧互,找到其中一個(gè)任務(wù)隊(duì)列執(zhí)行完畢挽铁,然后再執(zhí)行所有的micro-task伟桅,這樣一直循環(huán)下去。
  • 其中每一個(gè)任務(wù)的執(zhí)行叽掘,無(wú)論是macro-task還是micro-task楣铁,都是借助函數(shù)調(diào)用棧來(lái)完成。

舉個(gè)例子:

setTimeout(function() {
    console.log('timeout1');
})
 
new Promise(function(resolve) {
    console.log('promise1');
    for(var i = 0; i < 1000; i++) {
        i == 99 && resolve();
    }
    console.log('promise2');
}).then(function() {
    console.log('then1');
})
 
console.log('global1');

執(zhí)行結(jié)果為:
promise1
promise2
global1
then1
timeout1
分析一下代碼更扁,首先程序開(kāi)始執(zhí)行盖腕,遇到setTimeout時(shí)將它添加到消息隊(duì)列,等待后續(xù)處理浓镜,遇到Promise時(shí)會(huì)創(chuàng)建微任務(wù)(.then()里面的回調(diào))溃列,注意此時(shí)new promise構(gòu)造函數(shù)中的代碼還是同步執(zhí)行的,只有.then中的回調(diào)會(huì)被添加到微任務(wù)隊(duì)列膛薛。因此會(huì)連續(xù)輸出promise1和promise2听隐。繼續(xù)執(zhí)行到console.log('global1')輸出global1,到此調(diào)用棧中已經(jīng)為空哄啄。此時(shí)微任務(wù)隊(duì)列里有一個(gè)任務(wù).then雅任,宏任務(wù)隊(duì)列里也有一個(gè)任務(wù)setTimout。
microtask必然是在某個(gè)宏任務(wù)執(zhí)行的時(shí)候創(chuàng)建的咨跌,而在下一個(gè)宏任務(wù)開(kāi)始之前沪么,瀏覽器會(huì)對(duì)頁(yè)面重新渲染(task >> 渲染 >> 下一個(gè)task(從任務(wù)隊(duì)列中取一個(gè)))。同時(shí)锌半,在上一個(gè)宏任務(wù)執(zhí)行完成后禽车,渲染頁(yè)面之前,會(huì)執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)刊殉。也就是說(shuō)哭当,在某一個(gè)宏任務(wù)執(zhí)行完后,在重新渲染與開(kāi)始下一個(gè)宏任務(wù)之前冗澈,就會(huì)將在它執(zhí)行期間產(chǎn)生的所有微任務(wù)都執(zhí)行完畢(在渲染前)。因此會(huì)執(zhí)行.then輸出then1陋葡,然后進(jìn)行下一輪事件循環(huán)亚亲,取出任務(wù)隊(duì)列中的setTimeout輸出timeout1。
總結(jié)一下執(zhí)行機(jī)制:

  1. 執(zhí)行一個(gè)宏任務(wù)(棧中沒(méi)有就從事件隊(duì)列中獲雀汀)

  2. 執(zhí)行過(guò)程中如果遇到微任務(wù)捌归,就將它添加到微任務(wù)的任務(wù)隊(duì)列中

  3. 宏任務(wù)執(zhí)行完畢后,立即執(zhí)行當(dāng)前微任務(wù)隊(duì)列中的所有微任務(wù)(依次執(zhí)行)

  4. 當(dāng)前宏任務(wù)執(zhí)行完畢岭粤,開(kāi)始檢查渲染惜索,然后GUI線程接管渲染

  5. 渲染完畢后,JS引擎線程繼續(xù)剃浇,開(kāi)始下一個(gè)宏任務(wù)(從宏任務(wù)隊(duì)列中獲冉碚住)

參考文章:
https://www.cnblogs.com/jymz/p/7900439.html
https://juejin.im/post/5a6547d0f265da3e283a1df7#heading-6
https://jakearchibald.com/2015/tasks-microtasks-queues-and-schedules/

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末猎物,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子角塑,更是在濱河造成了極大的恐慌蔫磨,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,402評(píng)論 6 499
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件圃伶,死亡現(xiàn)場(chǎng)離奇詭異堤如,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)窒朋,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,377評(píng)論 3 392
  • 文/潘曉璐 我一進(jìn)店門(mén)搀罢,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人侥猩,你說(shuō)我怎么就攤上這事榔至。” “怎么了拭宁?”我有些...
    開(kāi)封第一講書(shū)人閱讀 162,483評(píng)論 0 353
  • 文/不壞的土叔 我叫張陵洛退,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我杰标,道長(zhǎng)兵怯,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,165評(píng)論 1 292
  • 正文 為了忘掉前任腔剂,我火速辦了婚禮媒区,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘掸犬。我一直安慰自己袜漩,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,176評(píng)論 6 388
  • 文/花漫 我一把揭開(kāi)白布湾碎。 她就那樣靜靜地躺著宙攻,像睡著了一般。 火紅的嫁衣襯著肌膚如雪介褥。 梳的紋絲不亂的頭發(fā)上座掘,一...
    開(kāi)封第一講書(shū)人閱讀 51,146評(píng)論 1 297
  • 那天,我揣著相機(jī)與錄音柔滔,去河邊找鬼溢陪。 笑死,一個(gè)胖子當(dāng)著我的面吹牛睛廊,可吹牛的內(nèi)容都是我干的形真。 我是一名探鬼主播,決...
    沈念sama閱讀 40,032評(píng)論 3 417
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼超全,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼咆霜!你這毒婦竟也來(lái)了邓馒?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 38,896評(píng)論 0 274
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤裕便,失蹤者是張志新(化名)和其女友劉穎绒净,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體偿衰,經(jīng)...
    沈念sama閱讀 45,311評(píng)論 1 310
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡挂疆,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,536評(píng)論 2 332
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了下翎。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缤言。...
    茶點(diǎn)故事閱讀 39,696評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖视事,靈堂內(nèi)的尸體忽然破棺而出胆萧,到底是詐尸還是另有隱情,我是刑警寧澤俐东,帶...
    沈念sama閱讀 35,413評(píng)論 5 343
  • 正文 年R本政府宣布跌穗,位于F島的核電站,受9級(jí)特大地震影響虏辫,放射性物質(zhì)發(fā)生泄漏蚌吸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,008評(píng)論 3 325
  • 文/蒙蒙 一砌庄、第九天 我趴在偏房一處隱蔽的房頂上張望羹唠。 院中可真熱鬧,春花似錦娄昆、人聲如沸佩微。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,659評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)哺眯。三九已至,卻和暖如春扒俯,著一層夾襖步出監(jiān)牢的瞬間族购,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 32,815評(píng)論 1 269
  • 我被黑心中介騙來(lái)泰國(guó)打工陵珍, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人违施。 一個(gè)月前我還...
    沈念sama閱讀 47,698評(píng)論 2 368
  • 正文 我出身青樓互纯,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親磕蒲。 傳聞我的和親對(duì)象是個(gè)殘疾皇子留潦,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,592評(píng)論 2 353

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