JavaScript async/await 的奇淫技巧


JavaScript async/await 的奇淫技巧


話說,最新的 ECMAScript 已經引入了 async/await 語法,讓開發者可以更容易控制非同步的程式邏輯,換言之,我們可以減少許多 callback 的使用,讓 JavaScript 這種單線程、事件驅動的程式語言更易讀、好寫。

關於 async/await 的基礎使用,有興趣的人可以參考舊文「JavaScript 好用的 async 異步函數!」,而本文將探討更多實際使用上的小技巧。

另外,瀏覽器不一定有支援 async/await,你可以在新版的 Node.js 上面測試本文的內容。

呼叫 async 函數與一般的函數沒有差別


想像一下,async 函數就是一個在執行後會回傳 Promise 物件的「普通函數」,和一般常見的函數的使用差異,僅僅只是 async 函數在執行後「不是回傳函數執行結果」。這代表我們可以把 async 函數當作一般函數來呼叫使用,用法一模一樣。

async/await 與 Promise 是可以共通的


非常有趣,async 函數與 Promise 其實能夠共通,這代表我們可以玩一些特別的組合技。所以,若要把 async/await 玩得通透,建議你盡量熟悉 Promise 的各種用法。

實現 delay 函數


過去因為單線程和事件驅動的關係,JavaScript 不可能實現一個沒有嚴重副作用的 delay 函數,所以取而代之的是使用 setTimeout() 加上 callback 來實現「一定時間後執行什麼工作」的需要。

不過來到 async/await 的世界後,我們可以一行行描述程式邏輯,無論是不是同步(Synchronous)的程式碼,所以我們可以用 Promise 來包裝 setTimeout(),以實現一個在 async 函數裡可以跑的 delay 函數:

// 實現一個等待函數
const delay = (interval) => {
    return new Promise((resolve) => {
        setTimeout(resolve, interval);
    });
};

const main = async () => {
    console.log('Starting...');

    // 等待五秒
    await delay(5000);
    
    console.log('Done after five seconds')
};

main();

與 .map() 的組合技


JavaScript 陣列裡常使用的 .map() 方法,但是 .map() 方法內的處理函數是同步的(synchronous),也就是如果我們想在裡面跑非同步的邏輯,是沒辦法等到我們非同步的工作完成的。

假設我們有一個陣列,然後使用 .map() 方法操作它:

const arr = [ 1, 2, 3, 4, 5 ];

let results = arr.map((num) => {
    return num + 1;
});

// [ 2, 3, 4, 5, 6 ]
console.log(results);

通常,如果我們想引入非同步邏輯,我們可以這樣做,直接代換 .map() 內的處理函數就可以:

const asyncWorker = async (num) => {
    // 非同步的工作,會做一段時間
};

let results = arr.map(async (num) => {
    // 等待非同步工作完成
    await asyncWorker(num);
    
    return num + 1;
});

特別注意,引入 async 以後,results 會是一堆的 Promise 物件,而不是一個數值陣列。而且 .map 並不會等 asyncWorker() 這個非同步的工作做完才回傳,你可以想像這是一種「射後不理」的機制。

等 .map() 裡的所有工作處理完


既然 async 函數被執行後,會回傳一個 Promise,這代表我們可以藉由 Promise 物件來得知工作什麼時候完成。所以我們可以這樣做:

const asyncWorker = async (num) => {
    // 非同步的工作,會做一段時間
};

let jobs = arr.map(async (num) => {
    // 等待非同步工作完成
    await asyncWorker(num);
    
    return num + 1;
});

// 當所有工作完成後,顯示執行內容
Promise.all(jobs).then((results) => {
    // [ 2, 3, 4, 5, 6 ]
    console.log(results);
});

用 await 取代 promise.then() 的使用方式


前面說到可以運用 Promise.all() 方法來等待所有的非同步工作完成,但最終還是回到了 callback 的模式進行等待。而且,總是有懶惰鬼開發者會把這些程式碼寫成一行,非常不好讀:

Promise.all(arr.map(async (num) => {
    // 等待非同步工作完成
    await asyncWorker(num);
    
    return num + 1;
})).then((results) => {
    // [ 2, 3, 4, 5, 6 ]
    console.log(results);
});

既然已經有 async/await 的環境,很多人會盡量讓自己的 context 處於 async 函數的環境之下,這時我們就可以用 await 來取代 Promise 的 .then() 方法:

const main = async () => {

    // 改用 await 等待 Promise 內的工作全部完成
    let results = await Promise.all(arr.map(async (num) => {
        // 等待非同步工作完成
        await asyncWorker(num);
        
        return num + 1;
    }));
    
    // [ 2, 3, 4, 5, 6 ]
    console.log(results);
};

main();

與 .reduce() 的組合技


如同 .map() 方法,.reduce() 是另一個常見的陣列處理方法之一,它也同樣不是一個非同步的方法。若引入 async/await,可以讓 .reduce() 擴展為一個依序處理非同步工作的工具,讓非同步工作一個處理完後下一個才接著做。

一個原始的 .reduce() 使用大概如下:

const arr = [ 1, 2, 3, 4, 5 ];

// 將陣列所有數值一一加總
let result = arr.reduce((accumulation, num) => {
    return accumulation + num;
}, 0);

// 15
console.log(result);

若引入 async/await,會變成這樣的形式:

const arr = [ 1, 2, 3, 4, 5 ];

const main = async () => {

    // 將陣列所有數值一一加總
    let result = await arr.reduce(async (prev, num) => {
    
        // 等待前一個工作完成,並得到前個工作的結果
        let accumulation = await prev;
    
        return accumulation + num;
    }, Promise.resolve(0));
    
    // 15
    console.log(result);
};

main();

這時裡面可以跑各式各樣非同步工作,如前面所提到的 delay 函數:

const arr = [ 1, 2, 3, 4, 5 ];

const main = async () => {

    // 將陣列所有數值一一加總
    let result = await arr.reduce(async (prev, num) => {
    
        // 等待前一個工作完成,並得到前個工作的結果
        let accumulation = await prev;
        
        // 非同步工作:等一秒
        await delay(1000);
        
        return accumulation + num;
    }, Promise.resolve(0));
    
    // 15
    console.log(result);
};

main();

後記


懂得使用 async/await 和 Promise 之後,其實有很多的玩法,邏輯的描述也更為多元和簡單,強烈建議一定要熟悉他。:-)

留言

張貼留言

這個網誌中的熱門文章

有趣的邏輯問題:是誰在說謊

Web 技術中的 Session 是什麼?

淺談 USB 通訊架構之定義(一)

淺談 USB 通訊架構之定義(二)

Reverse SSH Tunnel 反向打洞實錄