2016年7月27日 星期三

JavaScript 好用的 async 異步函數!

Standard

先聲明,async 異步函數是 ECMAScript 第七版(ES7)才被支援的語法和特性,目前 ES7 還沒有被大多數的 JavaScript Engine 所實作,如果你要使用,需要用到 babel 這類工具,先把此程式編譯轉換,讓其可在舊版本 JavaScript Engine 上執行。

如果你覺得以 co 模組來操作 Generator 很好用,你可以想像 async 異步函數就是原生的 co,幾乎是同樣的使用方式,同樣的使用概念,只不過不再需要使用 generator 和 yield 這類語法。如果你是個過不了在函數上有個醜陋「*」符號這一關的人,async 異步函數的使用方式應該會讓你感覺到舒服許多。

什麼是 async 異步函數(async functions)?


異步函數使用方式其實和一般的函數一樣,只不過在這函數之內的程式,可以用 await 的語法去執行並等待異步工作(如:Promise)而不需要使用到骯髒的 callback function。宣告並使用一個 async 異步函數,就是在定義函數時加上「async」,然後直接執行這個函數即可,簡單的範例如下:

async function myAsyncFunc() {
    console.log('Hello async functions!');
}

myAsyncFunc();

搭配 Promise 的使用


Promise 通常被大量用來管理非同步的工作,並讓開發者容易管理錯誤拋出等機制,一個典型的 Promise 使用如下:

var task = new Promise(function(resolve, reject) {

    // 執行一個非同步的工作,完成後呼叫帶入的 callback
    doAsyncTask(function(err) {

        // 有問題呼叫 reject,並帶入錯誤值
        if (err)
            return reject(err);

        // 成功呼叫 resolve 並帶入回傳值
        resolve('VALUE');
    });
});

// 使用 then 去執行並等待工作完成,成功會呼叫 callback,失敗則用 catch 去接收。
task
    .then(function(val) {
        console.log(val);
    })
    .catch(function(err) {
        console.log(err);
    });

如果在「異步函數」中呼叫以 Promise 包裝的工作,可以直接使用 await 語法:

async function myAsyncFunc() {
    var val = await task;
    console.log(val);
}

myAsyncFunc();
你會看到在異步函數中,程式邏輯會以「像是阻塞的方式進行」,await 會等到工作完成後,將回傳值回傳,然後才繼續下一行工作。要注意的是,因為看起來像程式會阻塞,熟悉 JavaScript 的人會不自覺開始害怕事件引擎被鎖死,但實際上 await 是以非同步的方式在進行,並不會卡住或影響事件引擎的運作。

搭配 Thunk 的使用


什麼是 Thunk?簡單來說就是一個處理函數,完成時會呼叫 callback 函數表示完成,實務上最常的用法會在外面包一層函數,創造一個 Closure,一個簡單的 Thunk 如下:

function myThunkFunc(thing) {

    return function(done) {

        setTimeout(function() {
            console.log(thing);
            done(null, 'World');
        }, 1000);
    };
}

異步函數裡面,我們可以這樣使用它:
async function myAsyncFunc() {
    var val = await myThunkFunc('Hello');
    console.log(val);
}

myAsyncFunc();

等待其他異步函數完成工作


當然,await 除了可以吃 Thunk 和 Promise 之外,也可以處理並等待其他的「異步函數」,如下:
async function anotherAsyncFunc(thing) {
    var val = await myThunkFunc(thing);
    return val;
}

async function myAsyncFunc() {
    var val = await anotherAsyncFunc('Hello');
    console.log(val);
}

myAsyncFunc();

錯誤處理


當 Promise 的 reject() 被呼叫,或是 Thunk 的 callback 函數被呼叫時,第一個參數不是 null,就代表這個異步工作是有錯誤發生的,如果要從 await 偵測這些錯誤訊息,需要使用 try-catch 去接這些錯誤訊息。

async function myAsyncFunc() {
    try {
        var val = await myThunkFunc('Hello');
    } catch(e) {
        console.log(e);
    }
}

myAsyncFunc();

舒服!到處使用異步函數


一旦你熟悉如何使用異步函數,你可以到處使用。其實他就像一般的函數一樣,他可以被當成一個 callback 來使用,像是下面這個例子,就把它當成 Promise 的處理函數:

var task = new Promise(async function(resolve, reject) {
    try {
        await doAsyncTask();
    } catch(e) {
        return reject(e);
    }

    resolve();
});

後記


如果你是原本就在使用 co 模組的人,應該會發現 async/await 根本就是一樣的東西,對你來說根本無痛,唯一有點麻煩的是,目前 JavaScript 仍然還沒有原生支援,需要 babel 一類的編譯器才能使用。但有不少人看重程式碼的簡潔和漂亮,已經大量使用了。

另外提到,Koa 2.0 因為完全採用 async/await 的方式,無限期處於不穩定版本。等到 async/await 被原生支援那一天, Koa 2.0 穩定版就會推出了,相信這一天就快要到來。