未最佳化之前的版本:
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>