JavaScript 的并發(fā)處理 -- Event Loop 微任務(wù)、宏任務(wù)的理解

前言: 本文是我通過閱讀文章和視頻得到一些個(gè)人理解;(鏈接:信息來源以供參考)。如果我有誤讀的地方, 非常歡迎指出。

首先我們看一下下面的代碼:

    setTimeout(() => {
        console.log('第1個(gè)定時(shí)器回調(diào)')
    }, 100)
    setTimeout(() => {
        console.log('第2個(gè)定時(shí)器回調(diào)')
    }, 0)
    
    console.log('打印3')

先預(yù)測(cè)一下,打印結(jié)果。然后復(fù)制代碼運(yùn)行驗(yàn)證一下。

如果你的答案是依次打印 3 、2 、1 那么恭喜你答對(duì)了。

但是需要問 2 個(gè)為什么:

1. js 不是單線程嗎?為什么可以一邊執(zhí)行代碼, 一邊執(zhí)行定時(shí)器?

其實(shí) JS 確實(shí)是單線程,也就是說 JS 代碼只能一行行壓入執(zhí)行棧中去執(zhí)行;完成后執(zhí)行下一行。而定時(shí)器,其實(shí)不是 js 在執(zhí)行; 它是 window 也就是瀏覽器提供的 API;瀏覽器負(fù)責(zé)定時(shí)器的執(zhí)行; 完成后再由 JS 的執(zhí)行棧去完成回調(diào)。這里就引出第二個(gè)問題, 回調(diào)在什么時(shí)候執(zhí)行呢?

2. 為什么延遲為 0ms 的定時(shí)器不是最先打???到底執(zhí)行順序是怎么定的?

第一個(gè)問題我們知道了, 不論定時(shí)器設(shè)定了多久的延遲; 都會(huì)交給瀏覽器去處理;然后再把回調(diào)函數(shù)傳遞回來。但是,不是直接傳遞到 JS 的執(zhí)行棧; 而是傳到 "任務(wù)對(duì)列" 中去排列; 等 JS 執(zhí)行棧里事情都做完了,才會(huì)從 "任務(wù)對(duì)列" 中依次選出排在最前面的回調(diào),加入只能棧去執(zhí)行。

event-loop.gif

以上面的代碼為例, 其過程如下:

  1. JS 運(yùn)行, 從上往下執(zhí)行代碼;
  2. 執(zhí)行到第一個(gè)定時(shí)器;把執(zhí)行過程交給瀏覽器;瀏覽器開始到計(jì)時(shí)了;
  3. 執(zhí)行棧不等它、繼續(xù)向下走;發(fā)現(xiàn)第二個(gè)定時(shí)器,還是交給瀏覽器; 0ms 后就完成了; 瀏覽器把回調(diào)函數(shù)放到任務(wù)對(duì)列里排隊(duì);
  4. 執(zhí)行棧不會(huì)立刻去管任務(wù)對(duì)列,因?yàn)樗€要繼續(xù)忙著往下走,執(zhí)行 '打印3'
  5. 此時(shí),執(zhí)行棧忙完了,才會(huì)去任務(wù)對(duì)列里找事情做;發(fā)現(xiàn)隊(duì)列里有一個(gè)回調(diào)函數(shù)(第二個(gè)定時(shí)器的); 執(zhí)行它 '第2個(gè)定時(shí)器回調(diào)';
  6. 此時(shí)執(zhí)行棧、任務(wù)對(duì)列都空了; 這點(diǎn)事可能幾微秒就處理完了;就開始等著;
  7. 瀏覽器終于完成了倒計(jì)時(shí) 100ms, 才把第一個(gè)定時(shí)器的回調(diào)放入任務(wù)對(duì)列;
  8. 執(zhí)行棧正閑著,于是從任務(wù)對(duì)列里拿出這個(gè)回調(diào), 執(zhí)行 '第1個(gè)定時(shí)器回調(diào)';
  9. 最終執(zhí)行棧、任務(wù)對(duì)列、瀏覽器都沒事做了。執(zhí)行結(jié)束。

除了定時(shí)器, promise, Ajax 請(qǐng)求, 事件監(jiān)聽,所有異步操作都是同樣的道理。一旦 JS 執(zhí)行到異步操作, 都一律拋給瀏覽器;等瀏覽器響應(yīng)之后,將回調(diào)函數(shù)加入"任務(wù)對(duì)列"。需要注意的是:任務(wù)隊(duì)列又分為宏任務(wù)(marco task queue)微任務(wù)(mirco task queue)。 promise.then, mutationObserver, process.nextTick 都屬于微任務(wù)。當(dāng)執(zhí)行棧為空時(shí), 會(huì)優(yōu)先完成微任務(wù)(mirco task queue);再去完成宏任務(wù)(marco task queue).

下面是一個(gè)相對(duì)復(fù)雜的例子,練習(xí)把這個(gè)執(zhí)行過程再捋一遍


    setTimeout(() => {
        console.log('第1個(gè)定時(shí)器回調(diào)')
    }, 0)

    setTimeout(() => {
        console.log('第2個(gè)定時(shí)器回調(diào)')
    }, 2000)
    
    console.log(1)
    
    document.addEventListener('click', () => {
        console.log('click')
    })
    
    console.log(2)
    
    Promise.resolve(10).then((val) => {
        console.log(val)
    })
    console.log(3)

其過程如下:

  1. JS 運(yùn)行, 從上往下執(zhí)行代碼;
  2. 執(zhí)行到第一個(gè)定時(shí)器,交給瀏覽器處理; 倒計(jì)時(shí)0ms完成, 回調(diào)函數(shù)進(jìn)入宏任務(wù)隊(duì)列(marco task queue);執(zhí)行到第二個(gè)定時(shí)器; 交給瀏覽器處理; 開始 2000ms 的倒計(jì)時(shí);
  3. 繼續(xù)執(zhí)行,打印 1;
  4. 繼續(xù)執(zhí)行到事件綁定; 交給瀏覽器;
  5. 繼續(xù)執(zhí)行,打印 2;
  6. 繼續(xù)執(zhí)行到 Promise, 交給瀏覽器;立刻resolve了10, 將回調(diào)函數(shù)加入微任務(wù)隊(duì)列(mirco task queue);
  7. 繼續(xù)執(zhí)行,打印 3
  8. 此時(shí)執(zhí)行棧也許只用了幾微秒,就把事情都做完了;你還沒來得及做任何一次點(diǎn)擊;此時(shí)任務(wù)對(duì)列里有 Promise.then 的回調(diào)和第一個(gè)定時(shí)器的回調(diào);此時(shí)已經(jīng)閑下來的執(zhí)行棧,優(yōu)先執(zhí)行微任務(wù)隊(duì)列(mirco task queue)里的Promise.then 的回調(diào),先執(zhí)行打印 10;完成后再去執(zhí)行宏任務(wù)隊(duì)列(mirco task queue)的定時(shí)器回調(diào)('第1個(gè)定時(shí)器回調(diào)');
  9. 此時(shí),你開始不斷點(diǎn)擊頁(yè)面;瀏覽器的事件監(jiān)聽將每次點(diǎn)擊的回調(diào)加入任務(wù)對(duì)列;執(zhí)行棧是如此之快,它不斷從任務(wù)對(duì)列拿出點(diǎn)擊事件的回調(diào)執(zhí)行,打印 click;執(zhí)行后執(zhí)行??樟耍秩ト蝿?wù)對(duì)列尋找;如此循環(huán);
  10. 在你點(diǎn)擊第三次和第四次之間,第二個(gè)定時(shí)器倒計(jì)時(shí)結(jié)束,把回調(diào)加入到任務(wù)對(duì)列;你的第四次點(diǎn)擊,因此,排在執(zhí)行棧處理了定時(shí)器回調(diào)之后('第2個(gè)定時(shí)器回調(diào)');
  11. 等你停止點(diǎn)擊之后,執(zhí)行棧將任務(wù)對(duì)列都執(zhí)行空了。瀏覽器仍舊在監(jiān)聽點(diǎn)擊事件; 如果你繼續(xù)點(diǎn)擊,瀏覽器會(huì)往任務(wù)對(duì)列添加回調(diào); 并觸發(fā)執(zhí)行棧繼續(xù)工作,執(zhí)行一個(gè)個(gè)回調(diào)。

從這里可以發(fā)現(xiàn), 為什么當(dāng)單線程的 JS 進(jìn)行異步操作時(shí),并不會(huì)阻斷整個(gè)頁(yè)面,因?yàn)闉g覽器幫助你把異步的操作都完成,并把回調(diào)函數(shù)排列在任務(wù)隊(duì)列。JS 只需要關(guān)注主線程中的工作; 然后在閑下來之后,不斷執(zhí)行任務(wù)隊(duì)列即可。

但實(shí)際上,也可以發(fā)現(xiàn)并非完全不影響; 當(dāng)某一個(gè)回調(diào)函數(shù)的任務(wù)太復(fù)雜,它會(huì)一直拖住執(zhí)行棧;讓消息隊(duì)列中排隊(duì)的其他回調(diào)函數(shù)無法執(zhí)行;此時(shí)就會(huì)出現(xiàn)諸如頁(yè)面渲染卡頓、交互事件響應(yīng)慢;定時(shí)器超時(shí)等現(xiàn)象。

寫在最后

瀏覽器運(yùn)行 Js 和在 Node 中,event-loop 的機(jī)制會(huì)有區(qū)別;如果對(duì) Node 的 event-loop 有興趣,可以查看官網(wǎng)的介紹和這兩篇文章《瀏覽器與Node的事件循環(huán)(Event Loop)有何區(qū)別?》、《Nodejs探秘:深入理解單線程實(shí)現(xiàn)高并發(fā)原理》

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

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

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