使用 setTimeout 拆解一些 CPU 密集型的執行任務

注销發表於2022-05-13

未最佳化之前的版本:

let i = 0;

let start = Date.now();

function count() {

  // do a heavy job
  for (let j = 0; j < 1e9; j++) {
    i++;
  }

  alert("Done in " + (Date.now() - start) + 'ms');
}

count();

上述 count 函式里的 for 迴圈的 i 累加,是一個 CPU 密集型任務,在執行完畢之前,JavaScript 引擎任務佇列裡的其他任務,沒有機會得到執行。

最佳化版本

將 i 從 1 累加到 1e9 的任務,拆解成 1000 個小的子任務。每個子任務執行完畢之後,呼叫 setTimeout 排程自身,這樣任務佇列裡其他任務有機會得到執行。

let i = 0;

let start = Date.now();

function count() {

  // do a piece of the heavy job (*)
  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  } else {
    setTimeout(count); // schedule the new call (**)
  }

}

count();
  • 第一輪任務執行:i=1...1000000
  • 第二輪任務執行:i=1000001..2000000

以此類推。

現在,如果在引擎忙於執行第 1 部分時出現新的輔助任務(例如 onclick 事件),它會排隊,然後在第 1 部分完成時執行,然後再執行下一部分。 在 count 執行之間,使用 setTimeout 定期返回事件迴圈,為 JavaScript 引擎提供了排程執行其他任務的機會,以對其他使用者操作做出反應。

let i = 0;

let start = Date.now();

function count() {

  // move the scheduling to the beginning
  if (i < 1e9 - 1e6) {
    setTimeout(count); // schedule the new call
  }

  do {
    i++;
  } while (i % 1e6 != 0);

  if (i == 1e9) {
    alert("Done in " + (Date.now() - start) + 'ms');
  }

}

count();

為瀏覽器指令碼拆分 CPU 密集型任務的另一個好處是我們可以顯示進度指示。

如前所述,只有在當前執行的任務完成後才會繪製對 DOM 的更改,無論當前執行的任務需要多長時間才能執行完畢。

下面是一個例子:

<div id="progress"></div>

<script>

  function count() {
    for (let i = 0; i < 1e6; i++) {
      i++;
      progress.innerHTML = i;
    }
  }

  count();
</script>

對 i 的更改要等到函式執行完成後才會顯示,所以我們只會看到最後一個值。

但我們也可能想在任務期間展示一些東西,例如一個進度條。

如果我們使用 setTimeout 將繁重的任務拆分為多個部分,那麼 i 會在多個部分執行之間繪製出來。

<div id="progress"></div>

<script>
  let i = 0;

  function count() {

    // do a piece of the heavy job (*)
    do {
      i++;
      progress.innerHTML = i;
    } while (i % 1e3 != 0);

    if (i < 1e7) {
      setTimeout(count);
    }

  }

  count();
</script>

相關文章