抬槓:寫個死迴圈,還要讓頁面正常跑

serialcoder發表於2019-04-08

最近在優化之前的練習程式碼時想到了半年前的一個小插曲。

當時我在掘金發了第二篇文章 -- 《不懂遞迴?讀完這篇保證你懂》。有位仁兄覺得我在炫技,和我槓上了。由於原文已經刪除了,我複述下對話吧。有精簡,無扭曲。

網友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);
複製程式碼

相關文章