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