【JS】1034- Event Loop :你知道它們的打印順序嗎?

前言
在 《瀏覽器知識(shí)點(diǎn)整理(十三)不同的回調(diào)執(zhí)行時(shí)機(jī):宏任務(wù)和微任務(wù)》[1] 這篇文章的后面有幾道打印面試題,覺得不夠過癮,于是又找了一些對(duì)應(yīng)的面試題來,還在不同的 Node.js 版本上面跑了跑,希望你也能過過癮。
先來回顧一下瀏覽器 Event Loop 的過程:
先執(zhí)行 當(dāng)前調(diào)用棧中的同步代碼(一個(gè)宏任務(wù)); 調(diào)用棧為空后去檢查是否有異步任務(wù)(微任務(wù))需要執(zhí)行; 如果有則 執(zhí)行完當(dāng)前異步代碼(當(dāng)前宏任務(wù)中微任務(wù)隊(duì)列里的所有微任務(wù)), 再之后就是 從消息隊(duì)列中取出下一個(gè)宏任務(wù) 去執(zhí)行(重新維護(hù)一個(gè)調(diào)用棧),然后開始新一輪的 Event Lopp。
然后不同版本的 Node.js 的一個(gè) Event Loop 區(qū)別:
如果是 Node 10 及其之前版本:宏任務(wù)隊(duì)列當(dāng)中有幾個(gè)宏任務(wù),是要等到宏任務(wù)隊(duì)列當(dāng)中的所有宏任務(wù)全部執(zhí)行完畢才會(huì)去執(zhí)行微隊(duì)列當(dāng)中的微任務(wù)。 如果是 Node 11 及之后版本:一旦執(zhí)行一個(gè)階段里對(duì)應(yīng)宏隊(duì)列當(dāng)中的一個(gè)宏任務(wù)( setTimeout,setInterval和setImmediate三者其中之一,不包括 I/O)就立刻執(zhí)行微任務(wù)隊(duì)列,執(zhí)行完微隊(duì)列當(dāng)中的所有微任務(wù)再回到剛才的宏隊(duì)列執(zhí)行下一個(gè)宏任務(wù)。這就 跟瀏覽器端運(yùn)行一致 了。
為了過這個(gè)癮,我意用 nvm (nvm install 10.13.0)安裝了 10.13.0 版本的 Node.js

參賽選手主要是宏任務(wù)代表 setTimeout 和微任務(wù)扛把子 Promise 及新貴 async/awiat 。
基礎(chǔ)版本
首先是單個(gè)任務(wù)的版本:
setTimeout:
console.log('script start');
setTimeout(() => {
console.log('setTimeout1')
}, 100)
setTimeout(() => {
console.log('setTimeout2')
}, 50)
console.log('script end');
復(fù)制代碼
在 Chrome 中有一個(gè) ProcessDelayTask 函數(shù),該函數(shù)會(huì)根據(jù)發(fā)起時(shí)間和延遲時(shí)間計(jì)算出到期的任務(wù),然后依次執(zhí)行這些到期的任務(wù)。執(zhí)行順序如下:

Promise:
console.log('script start');
new Promise((resolve) => {
console.log('promise1');
resolve();
console.log('promise1 end');
}).then(() => {
console.log('promise2');
})
console.log('script end');
復(fù)制代碼
在 Promise 內(nèi)部是 同步執(zhí)行 的,所以會(huì)有以下打印順序:

async/awiat:
async function async1() {
console.log('async1 start')
await async2();
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
console.log('script start')
async1();
console.log('script end')
復(fù)制代碼
執(zhí)行順序如下,這里需要注意的是 async2 函數(shù)也是同步執(zhí)行的。

以上的基礎(chǔ)版本就是同步和異步的區(qū)別了,因?yàn)槭菃蝹€(gè)任務(wù),也沒有其它復(fù)雜的場(chǎng)景,在 Node.js 中的表現(xiàn)和瀏覽器是一樣的。
組合版本一(setTimeout 和 Promise)
console.log('script start');
setTimeout(() => {
console.log('setimeout');
}, 0)
new Promise((resolve) => {
console.log('promise1');
resolve();
console.log('promise1 end');
}).then(() => {
console.log('promise2');
})
console.log('script end');
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

這個(gè)版本就是簡單的宏任務(wù) setTimeout 和微任務(wù) Promise 的組合了,因?yàn)橐簿鸵粋€(gè)宏任務(wù)和一個(gè)微任務(wù),它在不同版本中的 Node.js 里面表現(xiàn)也是和在瀏覽器一樣的。
組合版本二(setTimeout 和 Promise、async/await)
console.log('script start')
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
async1()
new Promise(resolve => {
console.log('promise1 start')
resolve()
console.log('promise1 end')
}).then(() => {
console.log('promise2')
})
console.log('script end')
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

在 Node.js 10.13.0 中的執(zhí)行順序:

在 Node.js 12.18.3 中的執(zhí)行順序:

驚不驚喜!意不意外!Chrome(91版本)和 Node.js 12.18.3 的版本表現(xiàn)是一致的,在 Node.js 10.13.0 版本中有些差異,主要是 promise2 和 async1 end 打印順序的不同,也就是說 async/await 在不同版本的 Node.js 處理是不一樣的(Chrome 70 后和 Chrome 70 前的處理也不一樣)。
想要搞清楚其中的區(qū)別,可以去 promise, async, await, execution order[2] 一探究竟。
混合版本一(setTimeout 和 Promise)
console.log('script start')
setTimeout(() => {
console.log('setTimeout1')
Promise.resolve().then(() => {
console.log('promise1')
})
}, 0)
setTimeout(() => {
console.log('setTimeout2')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('script end')
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

在 Node.js 10.13.0 中的執(zhí)行順序:

在 Node.js 12.18.3 中的執(zhí)行順序:

這個(gè)混合版本主要是看 Node.js 版本 10 前后 Event Loop 的差別。
混合版本二(setTimeout 和 Promise)
setTimeout 和 Promise:
console.log('script start')
Promise.resolve().then(() => {
console.log('promise1')
setTimeout(() => {
console.log('setTimeout1')
}, 0)
})
setTimeout(() => {
console.log('setTimeout2')
Promise.resolve().then(() => {
console.log('promise2')
})
}, 0)
console.log('script end')
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

在 Node.js 10.13.0 中的執(zhí)行順序:

在 Node.js 12.18.3 中的執(zhí)行順序:

這個(gè)版本是執(zhí)行完同步代碼(script end)后,這時(shí)候第一個(gè) Promise(promise1) 在微任務(wù)隊(duì)列里面,第二個(gè) setTimeout 已經(jīng)加到消息隊(duì)列(延遲隊(duì)列)尾部了;這時(shí)候去執(zhí)行微任務(wù),即打印 promise1,然后把第一個(gè) setTimeout 加到消息隊(duì)列(延遲隊(duì)列)尾部,所以會(huì)是先打印 setTimeout2、promise2 之后再打印 setTimeout1。
混合版本三(Promise)
宏任務(wù)和微任務(wù)的組合混合都差不多了,那來看看微任務(wù)之間的混合吧!
async function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
async function async2() {
console.log('async2');
Promise.resolve(async3()).then(() => {
console.log('async2 end');
})
}
async function async3() {
console.log('async3');
Promise.resolve().then(() => {
console.log('async3 end');
})
}
console.log('script start');
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

在 Node.js 10.13.0 中的執(zhí)行順序:

這個(gè)也容易理解,按照調(diào)用棧的出入棧順序,先執(zhí)行的是同步代碼,執(zhí)行到 async3 的時(shí)候才把 async3 end 加入微任務(wù)隊(duì)列,之后 async3() 函數(shù)出棧,回到 async2,把 async2 end 加入微任務(wù)隊(duì)列,后面的同理,于是就有了這個(gè)打印順序。
混合版本四(Promise 和 async/await)
async function async1() {
console.log('async1 start');
await async2();
console.log('async1 end');
}
async function async2() {
console.log('async2');
await async3()
console.log('async2 end')
}
async function async3() {
await console.log('async3');
console.log('async3 end')
}
console.log('script start');
async1();
new Promise((resolve) => {
console.log('promise1');
resolve();
}).then(() => {
console.log('promise2');
});
console.log('script end');
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

在 Node.js 10.13.0 中的執(zhí)行順序:

版本四和版本三的區(qū)別是將 async 函數(shù)里面的 Promise 換成了 await,而且版本三中的 async 函數(shù)里面沒有 await,都可以把 function 前面的 async 標(biāo)記拿掉的,不要被迷惑了哦。
在同步代碼執(zhí)行到 script end 之前我想都沒什么問題,async 里面都是有 await 的,所以在后面的代碼可以理解為如下形式:
Promise.resolve().then(() => {
console.log('async3 end');
Promise.resolve().then(() => {
console.log('async2 end');
Promise.resolve().then(() => {
console.log('async1 end');
})
})
})
Promise.resolve().then(() => {
console.log('promise2');
})
復(fù)制代碼
混合版本五(Promise 和 async/await)
function async1() {
console.log('async1 start');
Promise.resolve(async2()).then(() => {
console.log('async1 end');
})
}
function async2() {
console.log('async2');
Promise.resolve(async3()).then(() => {
console.log('async2 end');
})
}
async function async3() {
await console.log('async3');
console.log('async3 end');
}
console.log('script start');
async1();
new Promise(function (resolve) {
console.log('promise1');
resolve();
}).then(function () {
console.log('promise2');
});
console.log('script end');
復(fù)制代碼
在 Chrome 中的執(zhí)行順序:

在 Node.js 10.13.0 中的執(zhí)行順序:

通過前面版本四的考驗(yàn),這里應(yīng)該也難不倒你,這里只有 async3 里面有一個(gè) await,執(zhí)行到 script end 之前都一樣,到了 async3 end 后面的代碼可以轉(zhuǎn)化為以下形式思考:
Promise.resolve().then(() => {
console.log('async3 end');
Promise.resolve().then(() => {
console.log('async2 end');
})
})
Promise.resolve().then(() => {
console.log('async1 end');
})
Promise.resolve().then(() => {
console.log('promise2');
})
復(fù)制代碼
總結(jié)
紙上得來終覺淺,絕知此事要躬行。
關(guān)于本文
來源:起風(fēng)了Q
https://juejin.cn/post/6979244116199047204

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 120+ 篇原創(chuàng)文章
