我是這樣理解EventLoop的

我是這樣理解EventLoop的

在這里插入圖片描述

一、前言

??眾所周知,在使用javascript時(shí),經(jīng)常需要考慮程序中存在異步的情況,如果對異步考慮不周,很容易在開發(fā)中出現(xiàn)技術(shù)錯(cuò)誤和業(yè)務(wù)錯(cuò)誤。作為一名合格的javascript使用者,了解異步的存在和運(yùn)行機(jī)制十分重要且有必要;那么,異步究竟是何方神圣呢?我們不得不提Event Loop:也叫做事件循環(huán),是指瀏覽器或Node環(huán)境的一種解決javaScript單線程運(yùn)行時(shí)不會(huì)阻塞的一種機(jī)制,也就是實(shí)現(xiàn)異步的原理。作為一種單線程語言,javascript本身是沒有異步這一說法的,是由其宿主環(huán)境提供的(EventLoop優(yōu)秀文章網(wǎng)上有很多,這篇文章是自己的整合和理解)。
注意:Event Loop 并不是在 ECMAScript 標(biāo)準(zhǔn)中定義的,而是在 HTML 標(biāo)準(zhǔn)中定義的;

二、Event Loop知識鋪墊

??javascript代碼運(yùn)行時(shí),任務(wù)被分為兩種,宏任務(wù)(MacroTask/Task)微任務(wù)(MircoTask);Event Loop在執(zhí)行和協(xié)調(diào)各種任務(wù)時(shí)也將任務(wù)隊(duì)列分為Task QueueMircoTak Queue分別對應(yīng)管理宏任務(wù)(MacroTask/Task)微任務(wù)(MircoTask);作為隊(duì)列,Task QueueMircoTak Queue也具備隊(duì)列特性:先進(jìn)先出(FIFO—first in first out)。

1、微任務(wù)(MircoTask)

??在 HTML 標(biāo)準(zhǔn)中,并沒有明確規(guī)定 Microtask,但是實(shí)際開發(fā)中包含以下四種:

  • Promise中的then、catch、finally(原理參考:【js進(jìn)階】手撕Promise,一碼一解析 包懂
  • MutationObserver(監(jiān)視 DOM 變動(dòng)的API,詳情參考MDN
  • Object.observe(廢棄:監(jiān)聽標(biāo)準(zhǔn)對象的變化)
  • Process.nextTick(Node環(huán)境,通常也被認(rèn)為是微任務(wù))

2、宏任務(wù)(MacroTask/Task)

??基本上,我們將javascript中非微任務(wù)(MircoTask)的所有任務(wù)都?xì)w為宏任務(wù),比如:

  • script中全部代碼
  • DOM操作
  • 用戶交互操作
  • 所有的網(wǎng)路請求
  • 定時(shí)器相關(guān)的 setTimeout、setInterval 等
  • ···

3、javascript runtime

??javascript runtime:為 JavaScript 提供一些對象或機(jī)制,使它能夠與外界交互,是javascript的執(zhí)行環(huán)境。javascript執(zhí)行時(shí)會(huì)創(chuàng)建一個(gè)main thread主線程call-stack 調(diào)用棧(執(zhí)行棧,遵循后進(jìn)先出的規(guī)則),所有的任務(wù)都會(huì)被放到調(diào)用棧/執(zhí)行棧等待主線程執(zhí)行。其運(yùn)行機(jī)制如下:

在這里插入圖片描述

  • 1)主線程自上而下依次執(zhí)行所有代碼;
  • 2)同步任務(wù)直接進(jìn)入到主線程被執(zhí)行;
  • 3)異步任務(wù)進(jìn)入到Event Table,當(dāng)異步任務(wù)有結(jié)果后,將相對應(yīng)的回調(diào)函數(shù)進(jìn)行注冊,放入Event Queue;
  • 4)主線程任務(wù)執(zhí)行完空閑下來后,從Event Queue(FIFO)中讀取任務(wù),放入主線程執(zhí)行;
  • 5)放入主線程的Event Queue任務(wù)繼續(xù)從第一步開始,如此循環(huán)執(zhí)行;
    上述步驟執(zhí)行過程就是我們所說的事件循環(huán)(Event Loop),上圖展示了事件循環(huán)中的一個(gè)完整循環(huán)過程。

三、瀏覽器環(huán)境的Event Loop

??不同的執(zhí)行環(huán)境中,Event Loop的執(zhí)行機(jī)制是不同的;例如Chrome 和 Node.js 都使用了 V8 Engine:V8 實(shí)現(xiàn)并提供了 ECMAScript 標(biāo)準(zhǔn)中的所有數(shù)據(jù)類型、操作符、對象和方法(注意并沒有 DOM)。但它們的 Runtime 并不一樣:Chrome 提供了 window、DOM,而 Node.js 則是 require、process 等等。我們在了解瀏覽器中Event Loop的具體表現(xiàn)前需要先整理同步、異步、微任務(wù)、宏任務(wù)之間的關(guān)系!

1、同步、異步 和 宏任務(wù)、微任務(wù)

??看到這里,可能會(huì)有很多疑惑:同步異步很好理解,宏任務(wù)微任務(wù)上面也進(jìn)行了分類,但是當(dāng)他們四個(gè)在一起后就感覺很混亂了,冥冥之中覺得同步異步和宏任務(wù)微任務(wù)有內(nèi)在聯(lián)系,但是他們之間有聯(lián)系嗎?又是什么聯(lián)系呢?網(wǎng)上有的文章說宏任務(wù)就是同步的,微任務(wù)就是異步的 這種說法明顯是錯(cuò)的!
??其實(shí)我更愿意如此描述:宏任務(wù)和微任務(wù)是相對而言的,根據(jù)代碼執(zhí)時(shí)循環(huán)的先后,將代碼執(zhí)行分層理解,在每一層(一次)的事件循環(huán)中,首先整體代碼塊看作一個(gè)宏任務(wù),宏任務(wù)中的 Promise(then、catch、finally)、MutationObserver、Process.nextTick就是該宏任務(wù)層的微任務(wù);宏任務(wù)中的同步代碼進(jìn)入主線程中立即執(zhí)行的,宏任務(wù)中的非微任務(wù)異步執(zhí)行代碼將作為下一次循環(huán)的宏任務(wù)時(shí)進(jìn)入調(diào)用棧等待執(zhí)行的;此時(shí),調(diào)用棧中等待執(zhí)行的隊(duì)列分為兩種,優(yōu)先級較高先執(zhí)行的本層循環(huán)微任務(wù)隊(duì)列(MicroTask Queue),和優(yōu)先級低的下層循環(huán)執(zhí)行的宏任務(wù)隊(duì)列(MacroTask Queue)!
注意:每一次/層循環(huán),都是首先從宏任務(wù)開始,微任務(wù)結(jié)束;

在這里插入圖片描述

2、簡單實(shí)例分析

上面的描敘相對拗口,結(jié)合代碼和圖片分析理解:

在這里插入圖片描述

??答案暫時(shí)不給出,我們先進(jìn)行代碼分析:這是一個(gè)簡單而典型的雙層循環(huán)事件循環(huán)執(zhí)行案例,在這個(gè)循環(huán)中可以按照以下步驟進(jìn)行分析:

  • 1、首先區(qū)分出該層宏任務(wù)的范圍(整個(gè)代碼);
  • 2、區(qū)分宏任務(wù)同步代碼異步代碼
    同步代碼:console.log('script start');、console.log('enter promise');console.log('script end');
    異步代碼塊:setTimeoutPromise的then注意Promise中只有then、catch、finally的執(zhí)行需要等到結(jié)果,Promise傳入的回調(diào)函數(shù)屬于同步執(zhí)行代碼);
  • 3、在異步中找出同層的微任務(wù)(代碼中的Promise的then)和下層事件循環(huán)的宏任務(wù)(代碼中的setTimeout
  • 4、宏任務(wù)同步代碼優(yōu)先進(jìn)入主線程,按照自上而下順序執(zhí)行完畢;
    輸出順序?yàn)椋?/li>
//同步代碼執(zhí)行輸出
script start
enter promise
script end
  • 5、當(dāng)主線程空閑時(shí),執(zhí)行該層的微任務(wù)
//同層微任務(wù)隊(duì)列代碼執(zhí)行輸出
promise then 1
promise then 2
  • 6、首層事件循環(huán)結(jié)束,進(jìn)入第二層事件循環(huán)(setTimeout包含的執(zhí)行代碼,只有一個(gè)同步代碼)
//第二層宏任務(wù)隊(duì)列代碼執(zhí)行輸出
setTimeout

綜合分析最終得出數(shù)據(jù)結(jié)果為:

//首層宏任務(wù)代碼執(zhí)行輸出
script start
enter promise
script end
//首層微任務(wù)隊(duì)列代碼執(zhí)行輸出
promise then 1
promise then 2
//第二層宏任務(wù)隊(duì)列代碼執(zhí)行輸出
setTimeout

3、復(fù)雜案例分析

??那么,你是否已經(jīng)了解上述執(zhí)行過程了呢?如果完全理解上述實(shí)例,說明你已經(jīng)大概知道瀏覽器中Event Loop的執(zhí)行機(jī)制,但是,要想知道自己是不是完全明白,不妨對于下列多循環(huán)的事件循環(huán)進(jìn)行分析檢驗(yàn),給出你的結(jié)果:

console.log('1');

setTimeout(function() {
    console.log('2');
    new Promise(function(resolve) {
        console.log('3');
        resolve();
    }).then(function() {
        console.log('4')
    })
    setTimeout(function() {
        console.log('5');
        new Promise(function(resolve) {
            console.log('6');
            resolve();
        }).then(function() {
            console.log('7')
        })
    })
    console.log('14');
})

new Promise(function(resolve) {
    console.log('8');
    resolve();
}).then(function() {
    console.log('9')
})

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

分析:如下圖草稿所示,左上角標(biāo)a為宏任務(wù)隊(duì)列,左上角標(biāo)i為微任務(wù)隊(duì)列,同一層循環(huán)中,本層宏任務(wù)先執(zhí)行,再執(zhí)行微任務(wù);本層宏任務(wù)中的非微任務(wù)異步代碼塊作為下層循環(huán)的宏任務(wù)進(jìn)入下次循環(huán),如此循環(huán)執(zhí)行;

在這里插入圖片描述

如果你的與下面的結(jié)果一致,恭喜你瀏覽器環(huán)境的Event Loop你已經(jīng)完全掌握,那么請開始下面的學(xué)習(xí):

1->8->13->9->2->3->14->4->10->11->12->5->6->7

四、Node 環(huán)境下的 Event Loop

??在Node環(huán)境下,瀏覽器的EventLoop機(jī)制并不適用,切記不能混為一談。這里借用網(wǎng)上很多博客上的一句總結(jié)(其實(shí)我也是真不太懂):Node中的Event Loop是基于libuv實(shí)現(xiàn)的:libuvNode 的新跨平臺抽象層,libuv使用異步,事件驅(qū)動(dòng)的編程方式,核心是提供i/o的事件循環(huán)和異步回調(diào)。libuvAPI包含有時(shí)間,非阻塞的網(wǎng)絡(luò),異步文件操作,子進(jìn)程等等。

1、Event Loop的6階段

在這里插入圖片描述

??Node的Event loop一共分為6個(gè)階段,每個(gè)細(xì)節(jié)具體如下:

  • timers: 執(zhí)行setTimeout和setInterval中到期的callback。
  • pending callback: 上一輪循環(huán)中少數(shù)的callback會(huì)放在這一階段執(zhí)行。
  • idle, prepare:僅在內(nèi)部使用。
  • poll:最重要的階段,執(zhí)行pending callback,在適當(dāng)?shù)那闆r下回阻塞在這個(gè)階段。
  • check:執(zhí)行setImmediate的callback。
  • close callbacks: 執(zhí)行close事件的callback,例如socket.on('close'[,fn])或者h(yuǎn)ttp.server.on('close, fn)。
    注意:上面六個(gè)階段都不包括 process.nextTick()
    在這里插入圖片描述

重點(diǎn):如上圖所,在Node.js中,一次宏任務(wù)可以認(rèn)為是包含上述6個(gè)階段、微任務(wù)microtask會(huì)在事件循環(huán)的各個(gè)階段之間執(zhí)行,也就是一個(gè)階段執(zhí)行完畢,就會(huì)去執(zhí)行microtask隊(duì)列的任務(wù)。

2、process.nextTick()

??在第二節(jié)中就了解到,process.nextTick()屬于微任務(wù),但是這里需要重點(diǎn)提及下:

  • process.nextTick()雖然它是異步API的一部分,但未在圖中顯示。因?yàn)?code>process.nextTick()從技術(shù)上講,它不是事件循環(huán)的一部分;
  • 當(dāng)每個(gè)階段完成后,如果存在 nextTick,就會(huì)清空隊(duì)列中的所有回調(diào)函數(shù),并且優(yōu)先于其他 microtask 執(zhí)行(可以理解為微任務(wù)中優(yōu)先級最高的

3、實(shí)例分析

??老規(guī)矩,線上代碼:

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')
    })
})
process.nextTick(function() {
    console.log('6');
})
new Promise(function(resolve) {
    console.log('7');
    resolve();
}).then(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')

將代碼的執(zhí)行分區(qū)進(jìn)行解釋

在這里插入圖片描述

分析:如下圖草稿所示,左上角標(biāo)a為宏任務(wù)隊(duì)列,左上角標(biāo)i為微任務(wù)隊(duì)列,左上角標(biāo)t為timers階段隊(duì)列,左上角標(biāo)p為nextTick隊(duì)列同一層循環(huán)中,本層宏任務(wù)先執(zhí)行,再執(zhí)行微任務(wù);本層宏任務(wù)中的非微任務(wù)異步代碼塊作為下層循環(huán)的宏任務(wù)進(jìn)入下次循環(huán),如此循環(huán)執(zhí)行:
在這里插入圖片描述

  • 1、整體代碼可以看做宏任務(wù),同步代碼直接進(jìn)入主線程執(zhí)行,輸出1,7,13,接著執(zhí)行同層微任務(wù)且nextTick優(yōu)先執(zhí)行輸出6,8
  • 2、二層中宏任務(wù)中只存在setTimeout,兩個(gè)setTimeout代碼塊依次進(jìn)入6階段中的timer階段t1、t2進(jìn)入隊(duì)列;代碼等價(jià)于:
setTimeout(function() {
    console.log('2');
    process.nextTick(function() {
        console.log('3');
    })
    new Promise(function(resolve) {
        console.log('4');
        resolve();
    }).then(function() {
        console.log('5')
    })
})

setTimeout(function() {
    console.log('9');
    process.nextTick(function() {
        console.log('10');
    })
    new Promise(function(resolve) {
        console.log('11');
        resolve();
    }).then(function() {
        console.log('12')
    })
})
  • 3、setTimeout中的同步代碼立即執(zhí)行輸出2,4,9,11,nextTickPormise.then進(jìn)入微任務(wù)執(zhí)行輸出3,10,5,12
  • 4、二層中不存在6階段中的其他階段,循環(huán)完畢,最終輸出結(jié)果為:1->7->13->6->8->2->4->9->11->3->10->5->12

4、當(dāng)堂小考

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')
        setTimeout(function() {
          console.log('6');
          process.nextTick(function() {
              console.log('7');
          })
          new Promise(function(resolve) {
              console.log('8');
              resolve();
          }).then(function() {
              console.log('9')
          })
      })
    })
})
process.nextTick(function() {
    console.log('10');
})
new Promise(function(resolve) {
    console.log('11');
    resolve();
}).then(function() {
    console.log('12')
    setTimeout(function() {
      console.log('13');
      process.nextTick(function() {
          console.log('14');
      })
      new Promise(function(resolve) {
          console.log('15');
          resolve();
      }).then(function() {
          console.log('16')
      })
  })
})

setTimeout(function() {
    console.log('17');
    process.nextTick(function() {
        console.log('18');
    })
    new Promise(function(resolve) {
        console.log('19');
        resolve();
    }).then(function() {
        console.log('20')
    })
})
console.log('21')

五、總結(jié)

??瀏覽器Node環(huán)境下,microtask 任務(wù)隊(duì)列的執(zhí)行時(shí)機(jī)不同:Node 端,microtask 在事件循環(huán)的各個(gè)階段之間執(zhí)行;瀏覽器端,microtask 在事件循環(huán)的 macrotask 執(zhí)行完之后執(zhí)行;

參考借鑒

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

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

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