擡槓:寫個死循環,還要讓頁面正常跑

最近在優化以前的練習代碼時想到了半年前的一個小插曲。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);
複製代碼
相關文章
相關標籤/搜索