Promise


無論是 Node.js 或者是瀏覽器中,JavaScript 的運行流程,多半會是非同步方式,就目前來說,可以使用 setTimeout 來簡單地模擬非同步,例如:

function asyncFoo(n, callback) {
    setTimeout(() => {
        callback(n * Math.random());
    }, 2000);    
}

asyncFoo(10, r => console.log(r));

執行完 setTimeout 後,函式立即返回,而在時間到時,指定的回呼函式才會執行,在事情簡單時,這種方式沒什麼問題,然而,若希望事件發生時後,依序執行下一次非同步時,就會引發回呼地獄的問題:

asyncFoo(10, r1 => {
    asyncFoo(r1, r2 => {
        asyncFoo(r2, r3 => {
            console.log(r3);
        });
    });
});

就算使用了 ES6 的箭號函式,在這種非同步模式下,可讀性也是迅速降低,如果有個 asyncFoo 可以傳回 Promise,那事情會好辦的多:

function asyncFoo(n) {
    return new Promise(resolve => {
        setTimeout(() => {
            resolve(n * Math.random()); // 完成約定
        }, 2000);
    });
}

asyncFoo(10)
    .then(r1 => asyncFoo(r1))
    .then(r2 => asyncFoo(r2))
    .then(r3 => console.log(r3));

Promise 是 ES6 新增的 API,在建立 Promise 實例時,可以傳入一個回呼函式,該函式具有兩個參數,可命名為 resolvereject,這兩個參數會各自接受函式,若呼叫 resolve,表示此次 Promise 的任務完成,若 Promise 實例曾使用 then 組合下一次非同步操作,那麼會呼叫指定的下個函式,then 也會傳回一個 Promise,因此,雖說是非同步,然而撰寫風格上,就會像是循序的。

如果指定的任務無法達成,約定就無法滿足(fulfilled),此時可以由傳入的 reject 函式來背棄(reject)約定,例如:

new Promise((resolve, reject) => {
    let n = Math.floor(Math.random() * 10);
    if(n !== 0) {
        resolve(n);
    } else {
        reject('zero');
    }
}).then(
    n    => console.log(n),
    shit => console.log('shit happens', shit)
);

Promisethen 方法可以接受兩個函式,一個是在滿足約定時執行,另一個是在背棄約定被時執行,上面的例子在隨機數為 0 時會背棄約定。

如果約定在執行任務時發生例外,會隱含地背棄約定,當約定被背棄時,可以在 then 的第二個參數指定函式來處理,也可以使用 catch 指定函式來處理:

function dividedRandom(n) {
    return new Promise(resolve => {
        let r = n / Math.floor(Math.random() * 10);
        if(Number.isFinite(r)) {
            resolve(r);
        } else {
            throw 'divided by zero';
        }
    });
}

dividedRandom(10)
    .then(n => console.log(n))
    .catch(err => console.log(err));

當你只關心約定是否被背棄時,可以只撰寫 catch,從而避免了必須在 then 的第一個參數指定 nope 函式的情況:

dividedRandom(10)
    .then(
         () => {}, // 捨事都不做
         err => console.log(err)
    );

你可以有多個 then 來組合多個操作,然後接上一個 catch,只要先前的約定中,有某個非同步操作發生了錯誤,就會背棄約定,從而執行 catch 指定的函式。

如果你有多個 Promise,並不關心滿足約定的順序,只要最後的結果是按照指定約定的順序排列就可以時,可以使用 Promise.all,它接受一個 Promise 組成的陣列,例如:

Promise.all([dividedRandom(10), dividedRandom(10), dividedRandom(10)])
       .then(results => console.log(results[0], results[1], results[2]))
       .catch(err    => console.log(err));

如果你有多個 Promise,並不關心哪一個約定先滿足,只要有個約定滿足就可以的話,可以使用 Promise.race,它接受一個 Promise 組成的陣列,並傳回一個 Promise,只要其中有個約定先滿足或背棄,傳回的 Promise 就算滿足或背棄約定,陣列中其他約定依舊會繼續其任務,只是無論滿足或背棄,都不被 Promise.race 傳回的 Promise 考量,一個使用 Promise.race 的例子是:

Promise.race([dividedRandom(10), dividedRandom(10), dividedRandom(10)])
       .then(result => console.log(result))
       .catch(err   => console.log(err));

Promisethen 可以接受 Promise,如果不在乎前一個 Promise 的結果,只需要在前一個 Promise 完成後執行時使用。例如:

asyncFoo(10)
    .then(asyncFoo(20));

當約定與產生器結合時,可以產生有趣的操作風格,例如,若撰寫一個 async 函式如下:

function async(g) {
    let it = g();

    function consume(iteratorResult) {
        if(iteratorResult.done) {
            return;
        }

        let iteratorValue = iteratorResult.value;

        if(iteratorValue instanceof Promise) {
            iteratorValue.then(r    => consume(it.next(r)))
                         .catch(err => it.throw(err));
        } else {
            it.throw(`${iteratorValue} not a promise`);
        }
    }

    try {
        consume(it.next());
    } catch(err) {
        it.throw(err);
    }
}

async(function*() {
    let r1 = yield asyncFoo(10);
    let r2 = yield asyncFoo(r1);
    let r3 = yield asyncFoo(r2);
    console.log(r3);
});   

跟一開始的這個風格比比看:

asyncFoo(10)
    .then(r1 => asyncFoo(r1))
    .then(r2 => asyncFoo(r2))
    .then(r3 => console.log(r3));

async 的版本更像是語法層面的支援,實際上,在 ES8 中新增的 asyncawait 就在語法層面上提供了這類支援:

async function task() {
    let r1 = await asyncFoo(10);
    let r2 = await asyncFoo(r1);
    let r3 = await asyncFoo(r2);
    console.log(r3);
}

task();

你不用自行撰寫一個 async 函式了,而 await 在語意上,也會比 yield 來得清楚。