雖然在撰寫程式的時候,在大部份的情況下我們都想要讓程式執行速度愈快愈好。但有時候在寫測試程式或是爬蟲時,我們會希望讓程式等待一段時間再繼續執行,以模擬I/O延遲或是真人操作延遲的效果。不過JavaScript並沒有直接提供讓執行緒睡眠(sleep)的功能,要怎麼模擬出sleep函數呢?



先看看以下的程式:

let counter = 1;

for (;;) {
    console.log(counter);
    counter += 1;
}

以上程式會以飛快的速度印出不斷加一的數字。

如果我們想要藉由在迴圈內加上一個sleep函數來控制數字印出的速度,這個sleep函數該怎麼實作呢?

let counter = 1;

for (;;) {
    console.log(counter);
    sleep(1000); // wait for 1000ms
    counter += 1;
}

有些人可能會寫出如下的sleep函數:

// 不要用,這是爛Code!
const sleep = (milliseconds: number) => {
    const now = Date.now();

    while (Date.now() < now + milliseconds) {
        // do nothing
    }
};

這種實作方式雖然簡單直覺,但會使一個CPU邏輯核心的使用率變成100%,十分吃硬體資源,稱為忙碌等待(busy waiting)。程式會在沒多少干擾下不斷地循環執行迴圈的條件式,最好不要寫出這樣的程式。

比較好的作法是利用PromisesetTimeout函數來完成這個sleep函數。程式如下:

const sleep = (milliseconds: number): Promise<void> => {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
};

但如果我們用以上程式碼直接替換掉原本的sleep函數,重新執行主程式,卻會發現迴圈的執行速度根本就沒有被成功控制。因為現在這個sleep函數是非同步(asynchronous)執行的,它睡它的,程式依然還在執行。

我們可以利用async/await語法來改寫主程式,使主程式運行在非同步函數中,並且去對sleep函數做await的動作。程式改寫如下:

const sleep = (milliseconds: number): Promise<void> => {
    return new Promise((resolve) => setTimeout(resolve, milliseconds));
};

const main = async () => {
    let counter = 1;

    for (;;) {
        console.log(counter);
    
        await sleep(1000);
    
        counter += 1;
    }
};

void main();

您也可以觀察一下,在執行Promise版的sleep函數時的CPU佔用率,會發現它幾乎不佔用CPU了!

如果您還是想要一個同步(synchronous)版本的sleep函數,可以使用Atomics.wait函數來實作,同樣幾乎不佔用CPU。實作方式如下:

const sleep = (milliseconds: number) => {
    Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
};

let counter = 1;

for (;;) {
    console.log(counter);
    
    sleep(1000);
    
    counter += 1;
}

不過,SharedArrayBuffer雖然可以在Node.js的環境下被使用,但是在網頁瀏覽器上,只有在網頁有設定Cross-Origin-Opener-Policy: same-originCross-Origin-Embedder-Policy: require-corp這兩個HTTP標頭時才可以被使用。

呼叫Atomics.wait函數需傳入四個引入,第一個引數是一個只有一個元素的Int32Array(即陣列長度為1,因為SharedArrayBuffer的單位是位元組,需要4個位元組才能組成一個32位元的int),第二個引數則是設定要監看前面那個Int32Array的哪個索引位置,第三個引數則是設定監看的索引位置的值是否是Int32Array的那個索引位置的值。如果是的話,就等待第四個引數所設定的時間長度(毫秒),然後回傳字串timed-out;如果不是的話,就立刻回傳字串not-equal