ES6 Promises

Promises 作為ES6中非常重要的一個功能, 簡化了非同步函式的callback回傳方式, 也讓callback的層級回到caller的位置.

thenable object

{
    then(onFulfill, onReject){
        if(doSomething()){
            doSomethingAsync((err, res) => {
                if(err) onReject(err);
                else onFulfill("AsyncDone");
            });
        }else{
            onReject("SyncFailed");
        }
    }
}

thenable object是指一個含有then函式的object, 該函式會讀入兩個參數, 分別是成功完成的時候要執行的callback, 和失敗的時候執行的callback.
可以把then函式想成用來註冊callback用的函式(像是addEventListener之類的). onFulfill只會接受第一個參數作為結果, 如果要回傳多個值可以用Object或Array包起來. onReject的第一個參數則是拒絕的原因.

Promise

Promise可以想成是一個代表未來才會出現的值的一種抽象概念.

Status

Promise有三種狀態

  1. Pending: 還在處理回傳值的階段
  2. Fulfilled: 回傳值會傳給then裡的onFulfill參數
  3. Rejected: reject reason會傳給onReject函式
    Promise只會保證一件事, 這件事要嘛就完成, 要嘛失敗, 狀態一旦確定就不會再改變, 所以一旦Promise離開Pending就不會再改變狀態和結果.

Create & Promise.prototype.then

Promise也是一種thenable object, 他會在執行完的時候按結果呼叫onFulfill或是onReject.
下面來看看兩種建立Promise的方法:

var thenable1 = {
    then(onFulfill, onReject) {
        setTimeout(onFulfill, 100, 'done');
    }
}

// 或是直接把then函式傳進Promise的建構子裡面
// 這種方式會馬上執行傳進去的then函式
// 所以可以看到promiseB先輸出
// 一般比較常用這種方式建構Promise
var promiseA = Promise.resolve(thenable1)
promiseA.then(result => console.log(result))
var promiseB = new Promise(thenable1.then)
promiseB.then(result => console.log(result))

有點像是EventEmitter的.once, 但是不同的是, 如果eventListener太晚加上去可能會錯過event, 但是Promise會把結果固定並記錄下來, 所以在它被回收之前, 不管什麼時候用.then為它加上callback它都會再有結果之後把結果傳入callback中.

var a = new Promise((f, r) => setTimeout(f, 100, 1))
setTimeout(() => {
    a.then(res => { /* ... */ }) // res === 1
}, 100000)

Promise.resolve

Promise.resolve接受一個參數, 回傳一個Promise物件, 如果不是一個thenable object則會馬上進到Fulfilled狀態, 並把傳入的值作為結果; 如果是一個thenable object則會呼叫then後等待callback的呼叫.

var a = Promise.resolve('a');
a.then(result => console.log(result))

// thenable object則像上一個範例所示

Method Chaining & Return Value

因為Promise.prototype.then()回傳的會是另外一個Promise, 所以可以繼續接.then下去形成一條鍊

Promise.resolve(1)
.then(res => res*10)   // res === 1
.then(res => { // res === 10
}).then(res => { // res === undefined
})

如果在then裡面callback最後return的是另一個promise, 則會在該promise完成後繼續執行

Promise.resolve(1)
.then(res => new Promise(function(f, r){
        setTimeout(f, 1000, ['aa', (new Date()).getTime()])
    })
).then(res => {
    console.log(res[0]); // output: 'aa'
    console.log((new Date()).getTime() - r[1]) // output: 1000
})

然後也會建議then的callback盡量回傳Promise的型態, 可以使用resolve包起來

Promise.resolve(null)
.then(res => Promise.resolve(0))

Promise.prototype.catch && Promise.reject

前面講完了then要怎麼使用, 但還記得一開始在講thenable object的時候有講到除了onFulfill以外, 還有發生錯誤時要處裡的onReject嗎?
在Promise裡面.then也可以接受第二個callback引數做為onReject, 此外也可以使用.catch來添加.

a.then(res => { /* ... */ }, errReason => { /* ... */ })
// or
a.then(res => { /* ... */ })
.catch(errReason => { /* ... */ })

那要怎麼發出錯誤呢?如果是在一開始建構的時候有第二個onReject的引數可以使用, 而如果是在後面Method Chain中的callback中就是使用throw或回傳Promise.reject.

a.then(res => {
    if(!res) throw 'Error'
})
// or
a.then(res => {
    if(!res) return Promise.reject('Error')
})

跟前面的then一樣, 也是建議使用回傳Promise的方式.
錯誤傳遞的方式是Promise會一直沿著方法鍊搜下去, 直到找到第一個error Handler, 而在錯誤被處理之後, 如果沒再丟出錯誤則會還原為一個普通的promise繼續執行下去.

Promise.resolve(1)
.then(res => Promise.reject('e'))
.then(res => 2) // this WON'T evaluate
.catch(err => 3) // err === 'e'
.then(res => 4) // res === 3
.catch(err => 5) // this WON'T evaluate
.then(res => 6) // res === 4

Promise.all

前面說到Promise只會保證一件事, 那如果我想要在好幾件事都完成後才做另一件事呢, 那就是Promise.all登場的時候了, Promise.all接受一個Promise陣列作為引數, 回傳一個Promise物件, 回傳的Promise會在傳入的所有Promise都完成後被滿足.

var wait = (time) => new Promise( (f, r) => setTimeout(f, time, time) )
var a = wait(1000);
var b = wait(10);
var c = wait(500);

Promise.all([a,b,c]).then((res) => {
    /* Evaluate after 1000 ms */
    /* res == [1000, 10, 500] */
})

結果會按照當時輸入的順序排列, 如果有promise被reject則會把第一個被reject的結果作為結果

var wait = (time) => new Promise( (f, r) => setTimeout(f, time, time) )
var waitR = (time) => new Promise( (f, r) => setTimeout(r, time, time) )
var a = wait(100)
var b = waitR(1000)
var c = waitR(500)

Promise.all([a,b,c]).then((res) => {
    /* WON'T Evaluate */
}).then((res) => {
    /* Evaluate after 500 ms */
    /* res === 500 */
})

Multiple then

// 好像沒在規範中規定, 不過我在Chrome上試可以用
var wait = (time) => new Promise( (f, r) => setTimeout(f, time, time) )
var a = wait(100);
a.then(res => console.log(res))
a.then(res => console.log(res))
a.then(res => console.log(res))
/*
Console:
(After 100ms)
100
100
100
*/

實際例子

借用了Promisejs中的例子
如果我們想寫一個讀取JSON檔案的函式要怎麼寫呢?
原本同步的寫法:

function readJSONSync(filename) {
    return JSON.parse(fs.readFileSync(filename, 'utf8'));
}

很簡單的code, 但不幸的是這樣的寫法執行效率不太好, 讓我們換種作法.
非同步加錯誤處理的用法:

function readJSON(filename, callback){
    fs.readFile(filename, 'utf8', function (err, res){
        if (err) return callback(err);
        try {
            res = JSON.parse(res);
        } catch (ex) {
            return callback(ex);
        }
        callback(null, res);
    });
}

變得有點恐怖對吧, 讓我們用Promise重寫一下, 先把readFile包裝成Promise的類型

function readFile(filename, enc){
    return new Promise((f, r) => {
        fs.readFile(filename, enc, (err, res) => {
            if (err) r(err);
            else f(res);
        });
    });
}

然後就讓我們先直觀的寫寫看readJSON函式

function readJSON(filename){
  return new Promise((f, r) => {
    readFile(filename, 'utf8').then(res => {
      try {
        f(JSON.parse(res));
      } catch (ex) {
        r(ex);
      }
    }, r);
  });
}

看起來好像還是很複雜, 但其實有可以簡化的地方, 還記得如果在then中throw就會自動執行onReject嗎?

function readJSON(filename){
  return readFile(filename, 'utf8').then((res) => JSON.parse(res))
}

哦哦一下就變超簡短的!!!然後JSON.parse因為只是一個函式, 不用綁JSON物件, 而且參數順序一樣, 所以可以再縮

function readJSON(filename){
  return readFile(filename, 'utf8').then(JSON.parse)
}

到現在複雜度已經變得跟同步的寫法差不多了, 只差在回傳的是Promise object

async & generator

ES6 的 generator 提供了我們在函式中間暫停離開的能力, 如果搭上Promise的話則可以讓我們用近乎同步的寫法寫非同步的code, 就像前面所說的Promise可以想成一種代表未來數值的抽象概念, 程式執行到非同步的部分可以先跳出, 等取得結果再從原處繼續執行.
而在ES7中這就有了相對應的語法async & await, 幸運的是babel有提供async的語法支援!!!下面來看看async的語法

async function doSomething(){
    try{
        var a = await promiseA;
        var b = await promiseB;
        var c = await promiseC;
        var d = await rejectedPromiseD;
        var e = await promiseE;
    } catch(err){
        /* Evaluated due to the rejected Promise */
    }
}