深入理解Event Loop的運行機制

一、Event Loop是什么

Event Loop即事件循環(huán),是指瀏覽器或Node.js的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是我們經(jīng)常使用異步的原理。
Event Loop是一個執(zhí)行模型,在不同的地方有不同的實現(xiàn),瀏覽器和Node.js基于不同的技術(shù)實現(xiàn)了各自的Event Loop。

二、宏任務(wù)和微任務(wù)

宏任務(wù),macrotask,也叫tasks。一些異步任務(wù)的回調(diào)會依次進入macrotask queue,還有一部分會進入其他的隊列,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:

  • setTimeout
  • setInterval
  • setImmediate(Node和最新版本IE獨有)
  • I/O
  • requestAnimationFrame
  • requestIdleCallback

微任務(wù),microtask, 也叫jobs。另一些異步任務(wù)的回調(diào)會依次進入microtask queue,等待后續(xù)被調(diào)用,這些異步任務(wù)包括:

  • process.nextTick(Node獨有)
  • Promise.then
  • Object.observe
  • MutationObserver
    (注:這里只針對瀏覽器和Node.js)

三、Event Loop運行機制

瀏覽器的Event Loop的執(zhí)行過程:

1.執(zhí)行全局script的同步代碼。
2.執(zhí)行微任務(wù)隊列中的所有任務(wù)。
3.開始執(zhí)行macrotask宏任務(wù),從宏任務(wù)隊列中取一個任務(wù)出來執(zhí)行,然后又執(zhí)行所有的微任務(wù),執(zhí)行的流程為:執(zhí)行一個宏任務(wù) -> 步驟2 -> 執(zhí)行一個宏任務(wù) -> 步驟2 -> 執(zhí)行一個宏任務(wù)...

Node.js的Event Loop的執(zhí)行過程:

1.執(zhí)行全局script的同步代碼。
2.執(zhí)行所有microtask微任務(wù),先執(zhí)行Next Tick Queue中的所有任務(wù),從Next Tick Queue中依次取出任務(wù)放入調(diào)用棧中執(zhí)行,再執(zhí)行Other Microtask Queue中的所有任務(wù),也是從Other Microtask Queue中依次取出任務(wù)放入調(diào)用棧中執(zhí)行。
3.開始執(zhí)行macrotask宏任務(wù),共6個階段,從第1個階段開始執(zhí)行相應(yīng)每一階段macrotask中的所有任務(wù)(在瀏覽器的Event Loop中是每次只取宏任務(wù)隊列中的一個任務(wù)出來執(zhí)行,然后又執(zhí)行所有的微任務(wù)),每一個階段的macrotask執(zhí)行完畢后,又開始執(zhí)行所有微任務(wù),執(zhí)行的流程為:Timers Queue -> 步驟2 -> I/O Queue -> 步驟2 -> Check Queue -> 步驟2 -> Close Callback Queue -> 步驟2 -> Timers Queue ...

注意:在較新版本11.0中, Node.js為了向瀏覽器靠齊,對底部進行了修改,Node11及之后版本已經(jīng)把在timer階段的setTimeout,setInterval...和在check階段的setImmediate都修改為一旦執(zhí)行完一個階段里的一個任務(wù)就立刻執(zhí)行微任務(wù)隊列。

四、細節(jié)特性

NodeJS中微任務(wù)有兩種,分別是process.nextTick和promise.then,那么這兩個誰先執(zhí)行呢?

 Promise.resolve(‘123’).then(res=>{console.log(res)})
 Process.nextTick(()=>{console.log(‘nextTick’)})
 // 運行結(jié)果:
 // nextTick 
 // 123

解釋:
promise.then雖然和process.nextTick一樣,都將回調(diào)函數(shù)注冊到microtask微任務(wù)中,但優(yōu)先級不一樣,process.nextTick的microtask queue總是優(yōu)先于promise的microtask queue執(zhí)行。

setTimeout和setImmediate
在Node中,setTimeout和setImmediate執(zhí)行順序不固定 取決于Node的準(zhǔn)備時間。

 setTimeout(() => {
    console.log(‘setTimeout’)
 }, 0)
 setImmediate(() => {
    console.log(‘setImmediate’)
 })
 // 運行結(jié)果:
 // setImmediate
 // setTimeout
 // 或者
 // setTimeout
 // setImmediate

解釋:
setTimeout/setInterval的第二個參數(shù)取值范圍是:[1, 2^31 - 1],如果超過這個范圍則會初始化為1,即setTimeout(fn, 0) === setTimeout(fn, 1)。
我們知道setTimeout的回調(diào)函數(shù)在timer階段執(zhí)行,setImmediate的回調(diào)函數(shù)在check階段執(zhí)行,Event Loop開始會先檢查timer階段,但是在開始之前到timer階段會消耗一定時間,所以就會出現(xiàn)兩種情況:
1.timer前的準(zhǔn)備時間超過1ms,滿足loop(time >= 1),則執(zhí)行timer階段(setTimeout)的回調(diào)函數(shù)
2.timer前的準(zhǔn)備時間小于1ms,則不滿足loop(time < 1),會先執(zhí)行check階段(setImmediate)的回調(diào)函數(shù),下一次Event Loop執(zhí)行timer階段(setTimeout)的回調(diào)函數(shù)。

五、面試題解析

 console.log('1');

 setTimeout(function(){
    console.log('2');
    process.nextTick(function(){
       console.log('3')
    })
    new Promise(function(resolve){
        console.log('4');
        resolve();
    }).then(function(){
        console.log('5');
    })
 })

 new Promise(function(resolve){
    console.log('6');
    resolve();
 }).then(function(){
    console.log('7');
 })

 process.nextTick(function(){
    console.log('8');
 })

 setTimeout(function(){
    console.log('9');
    process.nextTick(function(){
       console.log('10')
    })
    new Promise(function(resolve){
       console.log('11');
       resolve();
    }).then(function(){
       console.log('12');
    })
 })

 console.log('13');

問:將以上代碼放在Node環(huán)境中(v11.0以下)運行,打印結(jié)果是什么?
答:1 6 13 8 7 2 4 9 11 3 10 5 12

解析:先執(zhí)行同步代碼,new Promise中的函數(shù)的代碼是同步代碼,Promise.then方法中函數(shù)的代碼才是異步的,所以第一輪循環(huán)同步代碼打印結(jié)果為:1 6 13,然后再看第一輪循環(huán)的微任務(wù),微任務(wù)總是會追加到本輪循環(huán)的末尾執(zhí)行,微任務(wù)中先執(zhí)行process.nextTick,再執(zhí)行Promise.then,所以第一輪循環(huán)打印結(jié)果為:1 6 13 8 7,此時第一輪微任務(wù)全都執(zhí)行完了,就該進入第二輪循環(huán)執(zhí)行宏任務(wù)了,宏任務(wù)分6個階段,這里只有Timer,所以打印結(jié)果為:1 6 13 8 7 2 4 9 11,然后執(zhí)行第二輪的微任務(wù),最后的打印結(jié)果為:1 6 13 8 7 2 4 9 11 3 10 5 12。

六、總結(jié)

1.Promise構(gòu)造函數(shù)里的代碼是同步執(zhí)行的,Promise.then里的代碼才是異步的。
2.不同環(huán)境中Event Loop的運行機制不同,所以不同環(huán)境中JS的運行結(jié)果也有可能不一致。
3.Node.js可以理解成有4個宏任務(wù)隊列和2個微任務(wù)隊列,但是執(zhí)行宏任務(wù)時有6個階段。

結(jié)語:這篇文章中只是介紹了像setTimeout和setInterval這些進入macrotask queue隊列的宏任務(wù)的執(zhí)行順序,想要了解更多,比如requestAnimationFrame和requestIdleCallback的執(zhí)行時機,可以看我的另一篇文章(requestAnimationFrame和requestIdleCallback是宏任務(wù)還是微任務(wù)), 希望本文章能對你有所幫助,文中有哪里不對的地方,歡迎大家指正,最后感謝大家的支持。

更多個人文章

  1. 兩個跨域頁面進行跳轉(zhuǎn)傳參的終極方案
  2. 徹底搞懂盒子模型
  3. hashHistory和browserHistory的區(qū)別
  4. 十分鐘帶你入門Chrome插件開發(fā)
  5. 面試秘籍之排序算法
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容