前言
在 《瀏覽器知識點整理(十三)不同的回調執行時機:宏任務和微任務》[1] 這篇文章的後面有幾道打印面試題,覺得不夠過癮,於是又找了一些對應的面試題來,還在不同的 Node.js 版本上面跑了跑,希望你也能過過癮。
先來回顧一下瀏覽器 Event Loop 的過程:
-
先執行 當前調用棧中的同步代碼(一個宏任務); -
調用棧爲空後去檢查是否有異步任務(微任務)需要執行; -
如果有則 執行完當前異步代碼(當前宏任務中微任務隊列裏的所有微任務), -
再之後就是 從消息隊列中取出下一個宏任務 去執行(重新維護一個調用棧),然後開始新一輪的 Event Lopp。
然後不同版本的 Node.js 的一個 Event Loop 區別:
-
如果是 Node 10 及其之前版本:宏任務隊列當中有幾個宏任務,是要等到宏任務隊列當中的所有宏任務全部執行完畢纔會去執行微隊列當中的微任務。 -
如果是 Node 11 及之後版本:一旦執行一個階段裏對應宏隊列當中的一個宏任務( setTimeout
,setInterval
和setImmediate
三者其中之一,不包括 I/O)就立刻執行微任務隊列,執行完微隊列當中的所有微任務再回到剛纔的宏隊列執行下一個宏任務。這就 跟瀏覽器端運行一致 了。
爲了過這個癮,我意用 nvm
(nvm install 10.13.0
)安裝了 10.13.0
版本的 Node.js
參賽選手主要是宏任務代表 setTimeout
和微任務扛把子 Promise
及新貴 async/awiat
。
基礎版本
首先是單個任務的版本:
setTimeout
:
console.log('script start');
setTimeout(() => {
console.log('setTimeout1')
}, 100)
setTimeout(() => {
console.log('setTimeout2')
}, 50)
console.log('script end');
複製代碼
在 Chrome 中有一個 ProcessDelayTask
函數,該函數會根據發起時間和延遲時間計算出到期的任務,然後依次執行這些到期的任務。執行順序如下:
Promise
:
console.log('script start');
new Promise((resolve) => {
console.log('promise1');
resolve();
console.log('promise1 end');
}).then(() => {
console.log('promise2');
})
console.log('script end');
複製代碼
在 Promise
內部是 同步執行 的,所以會有以下打印順序:
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')
複製代碼
執行順序如下,這裏需要注意的是 async2
函數也是同步執行的。
以上的基礎版本就是同步和異步的區別了,因爲是單個任務,也沒有其它複雜的場景,在 Node.js 中的表現和瀏覽器是一樣的。
組合版本一(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');
複製代碼
在 Chrome 中的執行順序:
這個版本就是簡單的宏任務 setTimeout
和微任務 Promise
的組合了,因爲也就一個宏任務和一個微任務,它在不同版本中的 Node.js 裏面表現也是和在瀏覽器一樣的。
組合版本二(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')
複製代碼
在 Chrome 中的執行順序:
在 Node.js 10.13.0
中的執行順序:
在 Node.js 12.18.3
中的執行順序:
驚不驚喜!意不意外!Chrome(91版本)和 Node.js 12.18.3
的版本表現是一致的,在 Node.js 10.13.0
版本中有些差異,主要是 promise2
和 async1 end
打印順序的不同,也就是說 async/await
在不同版本的 Node.js 處理是不一樣的(Chrome 70 後和 Chrome 70 前的處理也不一樣)。
想要搞清楚其中的區別,可以去 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')
複製代碼
在 Chrome 中的執行順序:
在 Node.js 10.13.0
中的執行順序:
在 Node.js 12.18.3
中的執行順序:
這個混合版本主要是看 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')
複製代碼
在 Chrome 中的執行順序:
在 Node.js 10.13.0
中的執行順序:
在 Node.js 12.18.3
中的執行順序:
這個版本是執行完同步代碼(script end
)後,這時候第一個 Promise
(promise1
) 在微任務隊列裏面,第二個 setTimeout
已經加到消息隊列(延遲隊列)尾部了;這時候去執行微任務,即打印 promise1
,然後把第一個 setTimeout
加到消息隊列(延遲隊列)尾部,所以會是先打印 setTimeout2
、promise2
之後再打印 setTimeout1
。
混合版本三(Promise
)
宏任務和微任務的組合混合都差不多了,那來看看微任務之間的混合吧!
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');
複製代碼
在 Chrome 中的執行順序:
在 Node.js 10.13.0
中的執行順序:
這個也容易理解,按照調用棧的出入棧順序,先執行的是同步代碼,執行到 async3
的時候才把 async3 end
加入微任務隊列,之後 async3()
函數出棧,回到 async2
,把 async2 end
加入微任務隊列,後面的同理,於是就有了這個打印順序。
混合版本四(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');
複製代碼
在 Chrome 中的執行順序:
在 Node.js 10.13.0
中的執行順序:
版本四和版本三的區別是將 async
函數裏面的 Promise
換成了 await
,而且版本三中的 async
函數裏面沒有 await
,都可以把 function
前面的 async
標記拿掉的,不要被迷惑了哦。
在同步代碼執行到 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');
})
複製代碼
混合版本五(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');
複製代碼
在 Chrome 中的執行順序:
在 Node.js 10.13.0
中的執行順序:
通過前面版本四的考驗,這裏應該也難不倒你,這裏只有 async3
裏面有一個 await
,執行到 script end
之前都一樣,到了 async3 end
後面的代碼可以轉化爲以下形式思考:
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');
})
複製代碼
總結
紙上得來終覺淺,絕知此事要躬行。
關於本文
來源:起風了Q
https://juejin.cn/post/6979244116199047204
回覆“加羣”與大佬們一起交流學習~
點擊“閱讀原文”查看 120+ 篇原創文章
本文分享自微信公衆號 - 前端自習課(FE-study)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。