前言: 本文是我通過閱讀文章和視頻得到一些個(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í)行。

以上面的代碼為例, 其過程如下:
- JS 運(yùn)行, 從上往下執(zhí)行代碼;
- 執(zhí)行到第一個(gè)定時(shí)器;把執(zhí)行過程交給瀏覽器;瀏覽器開始到計(jì)時(shí)了;
- 執(zhí)行棧不等它、繼續(xù)向下走;發(fā)現(xiàn)第二個(gè)定時(shí)器,還是交給瀏覽器; 0ms 后就完成了; 瀏覽器把回調(diào)函數(shù)放到任務(wù)對(duì)列里排隊(duì);
- 執(zhí)行棧不會(huì)立刻去管任務(wù)對(duì)列,因?yàn)樗€要繼續(xù)忙著往下走,執(zhí)行
'打印3'; - 此時(shí),執(zhí)行棧忙完了,才會(huì)去任務(wù)對(duì)列里找事情做;發(fā)現(xiàn)隊(duì)列里有一個(gè)回調(diào)函數(shù)(第二個(gè)定時(shí)器的); 執(zhí)行它
'第2個(gè)定時(shí)器回調(diào)'; - 此時(shí)執(zhí)行棧、任務(wù)對(duì)列都空了; 這點(diǎn)事可能幾微秒就處理完了;就開始等著;
- 瀏覽器終于完成了倒計(jì)時(shí) 100ms, 才把第一個(gè)定時(shí)器的回調(diào)放入任務(wù)對(duì)列;
- 執(zhí)行棧正閑著,于是從任務(wù)對(duì)列里拿出這個(gè)回調(diào), 執(zhí)行
'第1個(gè)定時(shí)器回調(diào)'; - 最終執(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)
其過程如下:
- JS 運(yùn)行, 從上往下執(zhí)行代碼;
- 執(zhí)行到第一個(gè)定時(shí)器,交給瀏覽器處理; 倒計(jì)時(shí)0ms完成, 回調(diào)函數(shù)進(jìn)入
宏任務(wù)隊(duì)列(marco task queue);執(zhí)行到第二個(gè)定時(shí)器; 交給瀏覽器處理; 開始 2000ms 的倒計(jì)時(shí); - 繼續(xù)執(zhí)行,打印
1; - 繼續(xù)執(zhí)行到事件綁定; 交給瀏覽器;
- 繼續(xù)執(zhí)行,打印
2; - 繼續(xù)執(zhí)行到 Promise, 交給瀏覽器;立刻resolve了10, 將回調(diào)函數(shù)加入
微任務(wù)隊(duì)列(mirco task queue); - 繼續(xù)執(zhí)行,打印
3; - 此時(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)'); - 此時(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); - 在你點(diǎn)擊第三次和第四次之間,第二個(gè)定時(shí)器倒計(jì)時(shí)結(jié)束,把回調(diào)加入到任務(wù)對(duì)列;你的第四次點(diǎn)擊,因此,排在執(zhí)行棧處理了定時(shí)器回調(diào)之后(
'第2個(gè)定時(shí)器回調(diào)'); - 等你停止點(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ā)原理》