最近在優化之前的練習程式碼時想到了半年前的一個小插曲。
當時我在掘金發了第二篇文章 -- 《不懂遞迴?讀完這篇保證你懂》。有位仁兄覺得我在炫技,和我槓上了。由於原文已經刪除了,我複述下對話吧。有精簡,無扭曲。
網友A:你寫這麼難指望誰能看懂?說得不好聽就是炫耀技術了。
我:能讓你有機會理解你還不懂的東西,你應該感謝才對。
網友A:就你牛逼,這麼牛逼,給你出個題:寫個死迴圈,還不影響頁面效能。不是牛逼麼,不要說你寫不出來啊。
我:剛好我上一篇文章就寫了個死迴圈,服不服?
……
上面是同行交流反面案例,大家不要跟著學。
我說的那個死迴圈很多人都看過了,長這樣:
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);
複製程式碼