ES6 非同步程式設計之二:Promise

碼農彭盛發表於2017-02-08

非同步回撥的泥潭

非同步回撥是最直接的非同步結果處理模式,將一個回撥函式callback扔進非同步處理函式中,當非同步處理獲得結果之後再呼叫這個回撥函式就可以繼續之後的處理,但是如果這個callback又是一個非同步呼叫呢?眾所周知的,在JavaScript中非同步回撥的層層巢狀幾乎是反人性的,程式碼編寫、修改和閱讀對我等有程式碼潔癖的人而言是一種煎熬,這便是非同步回撥泥潭了。

今天對於處理非同步呼叫已經有了很多成熟的方案,在我看來這些方案都無外乎在解決一個問題:“如何能看似順序地傳遞非同步呼叫的結果?”,本文要說的Promise就是ES6原生提供的一個解決方案。

在對Promise進行敘述之前,依舊引用阮大的《ECMAScript 6入門》一書中的Promise章節便於大家更嚴謹和全面的學習和參考。

Promise

承諾,即對未來的許諾,如果諾言實現,然後then)就如何如何……Promise極其生動的講述了一個言出必行的故事。


    new Promise(function(resolve, reject){
        //開始實現承諾
        ....
        ....
        if(承諾兌現時) {
           resolve(dollars);  //兌現承諾的結果是得到`一大筆美金`
        } else {
           reject(`絕交`);  //沒兌現承諾就絕交
        }
    }).then(function(dollars){  //然後有錢了,買房買車娶妻生子
       let d1 = buyHouse(dollars); //把每次消費剩餘的錢傳給下一個函式
       let d2 = buyCar(d1);
       let d3 = marry(d2);
       makeBaby(d3);
    }).catch(function(result){//然後如果絕交了,還是繼續吃土
       //繼續吃土
    });
    console.log(`故事開始....`);

看過上面的這個俗不可耐的故事之後需要理解幾件事情:

  1. 言出必行:一個Promise構造出來之後,構造時傳入的非同步函式就立即執行;*
    注:因大凡使用promise都是在非同步呼叫場景,下文所說的非同步函式都是指構造promise時傳入的函式*

  2. Promise例項內部維護了一個狀態機,狀態變化只可能是pendingresolved或者pendingrejected;

    • 執行resolvepending變化到resolved

    • 執行rejectpending變化到rejected

    • 丟擲錯誤:pending變化到rejected

  3. then的第一個回撥函式只會在發生了resolve之後執行,本質上是在Promise到達resolved狀態執行;

  4. then的第二個回撥函式或者catch的回撥函式會在發生reject之後或者非同步函式執行丟擲錯誤時執行,本質上是在promise到達rejected狀態時執行;

  5. 非同步函式執行得到結果可以通過resolve或者reject將結果傳出;

    • 呼叫resolve傳入的值會作為then第一個回撥函式的入參

    • 呼叫reject傳入的值作為then第二個回撥函式或者catch的回撥函式的入參

    • 如果非同步函式丟擲了異常,異常會作為then第二個回撥函式或者catch的回撥函式的入參

  6. `故事開始….`會先輸出,而不是等到then的回撥函式執行完畢才輸出,說明傳入then的回撥函式是非同步執行,同理catch也是一樣;

非同步函式呼叫鏈

thencatch都是Promise的例項方法,都返回一個新的Promise,因此可以輕而易舉地實現鏈式程式設計,比如上面的例子中“把每次消費剩餘的錢”傳給下一個函式可以改寫成這樣:

....//前面省略

     .then(function(dollars){  
           return buyHouse(dollars);
        }).then(function(d1){
            return buyCar(d1);
        }).then(function(d2){
            return marry(d2);
        }).then(function(d3){
            return makeBaby(d3);
        }).catch(function(result){
           //繼續吃土
        });

看到這裡你可能認為前一個then回撥函式的返回值是後一個then的回撥函式的入參,但這是不準確的,因為當then回撥函式返回的是個Promise物件時,這個Promise物件到終態時後一個then才會執行,並且該Promise物件執行resolve時的入參才是後一個then的回撥函式入參;

此時有必要對Promise的一個類方法resolve做以下說明,它的特性兩句話:

  1. 如果傳入的是個Promise物件,則直接返回這個Promise

  2. 如果是其他任何一個值(包括Error物件和undefined)則直接轉換為一個resolved狀態的Promise物件;

比如說下面的程式碼:


    //以下的p1和p2邏輯上等同
    let p1 = Promise.resolve(1);
    let p2 = new Promise(function(resolve, reject) {
        resolve(1);
    });
    
    //以下的p3和p4等同
    let p3 = new Promise(function(r, j) {});
    let p4 = Promise.resolve(p3);
    
    console.log(p3 == p4); //true
    console.log(p3 === p4); //true
    
    //以下三者邏輯上等同
    Promise.resolve().then(function(dollars) {
        return 1 + 1;
    }).then(function(v) {
        console.log(v);
    });
    Promise.resolve().then(function(dollars) {
        return new Promise(function(r, j) { r(1 + 1) });
    }).then(function(v) {
        console.log(v);
    });
    Promise.resolve().then(function(dollars) {
        return Promise.resolve(1 + 1);
    }).then(function(v) {
        console.log(v);
    });

我們可以利用Promise非同步執行結果傳出的機制和then的鏈式呼叫,將層層巢狀的函式呼叫變為通過then順序連線的鏈式呼叫
從寫法和形式上看是不是人性很多呢?

通過Promise實現的鏈式非同步函式呼叫,以斐波那契數列舉例如下:


//一個非同步的斐波那契計算
function fibonacci(v) { 
    return new Promise(function(resolve, reject) {  //每一個非同步呼叫都返回了一個Promise
        setTimeout(function() {
            console.log(`${v.a}`);
            [v.a, v.b] = [v.b, v.a + v.b];
            resolve(v);
        }, 500);
    });
}

//以下兩者邏輯等同,每個then都等待上一個promise的結果形成一條鏈。

// fibonacci({ a: 0, b: 1 })
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci)
//     .then(fibonacci);

Promise.resolve()
    .then(() => fibonacci({ a: 0, b: 1 }))
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci)
    .then(fibonacci);

相關文章