最近在優化以前的練習代碼時想到了半年前的一個小插曲。promise
當時我在掘金髮了第二篇文章 -- 《不懂遞歸?讀完這篇保證你懂》。有位仁兄以爲我在炫技,和我槓上了。因爲原文已經刪除了,我複述下對話吧。有精簡,無扭曲。異步
網友A:你寫這麼難期望誰能看懂?說得很差聽就是炫耀技術了。async
我:能讓你有機會理解你還不懂的東西,你應該感謝纔對。oop
網友A:就你牛逼,這麼牛逼,給你出個題:寫個死循環,還不影響頁面性能。不是牛逼麼,不要說你寫不出來啊。性能
我:恰好我上一篇文章就寫了個死循環,服不服?優化
……ui
上面是同行交流反面案例,你們不要跟着學。spa
我說的那個死循環不少人都看過了,長這樣:code
const starks = [
"Eddard Stark",
"Catelyn Stark",
"Rickard Stark",
"Brandon Stark",
"Rob Stark",
"Sansa Stark",
"Arya Stark",
"Bran Stark",
"Rickon Stark",
"Lyanna Stark"
];
function* repeatedArr(arr) {
let i = 0;
while (true) {
yield arr[i++ % arr.length];
}
}
const infiniteNameList = repeatedArr(starks);
const wait = ms =>
new Promise(resolve => {
setTimeout(resolve, ms);
});
(async () => {
for (const name of infiniteNameList) {
await wait(1000);
console.log(name);
}
})();
複製代碼
爲了證實這個死循環不影響頁面性能,我寫了個 codepen,在循環開始後,輸入框還能正常輸入。遞歸
因爲 codepen 會限制死循環,當wait
時間小於 1000 ms 時,codepen 會終止程序。不過你能夠把代碼保存到本地跑,把 wait 時間改爲 0 都沒問題。
之因此這樣寫沒讓頁面卡死,是由於 setTimeout 和 JavaScript 的事件循環機制。當 event loop 遇到 timeout 事件時,會將此任務推到 task queue 排隊,event loop 繼續處理調用棧,直到調用棧空了再來處理 task queue。
將上面的代碼簡化,依然利用 setTimeout 來實現死循環的功能:
let i = 0;
let timer = 0;
function start() {
p.innerText = starks[i++ % starks.length];
timer = setTimeout(start);
}
複製代碼
這個無限遞歸不會爆棧,也不會影響頁面性能。輸入框照常能輸入。見 codepen
既然都是異步事件,用 promise 能夠實現 setTimeout 的這個效果嗎?這就涉及到 task 和 micro task 的區別了。來試試:
let i = 0
function andThen(){
p.innerText = starks[i++ % starks.length];
Promise.resolve().then(andThen)
}
function start(){
Promise.resolve().then(andThen)
}
複製代碼
效果見這個 codepen。點擊開始後,頁面會卡死。
promise 屬於 micro task,當運行時處理完每一個 task 以後,都會檢查 micro task queue,若是不爲空,則將其依次執行完。上面無限遞歸生成無限個 micro task,事件循環一直執行 micro tasks,在處理完以前不響應其它事件,因此頁面會卡死。
本文開頭提到的優化歷史代碼,優化前 (codepen):
async function run(pause) {
for (tasks of chunkedTasks) {
await asyncPipe(...tasks)();
await wait(pause);
}
return run(pause);
}
run(1000);
複製代碼
優化後 (codepen):
async function run(pause) {
for (const tasks of chunkedTasks) {
await asyncPipe(...tasks)();
await wait(pause);
}
setTimeout(run, 0, pause);
}
run(1000);
複製代碼