Javascript異步編程之三Promise: 像堆積木同樣組織你的異步流程

這篇有點長,不過乾貨挺多,既分析promise的原理,也包含一些最佳實踐,亮點在最後:)javascript

還記得上一節講回調函數的時候,第一件事就提到了異步函數不能用return返回值,其緣由就是在return語句執行的時候異步代碼尚未執行完畢,因此return的值不是指望的運算結果。html

Promise卻偏偏要回過頭來從新利用這個return語句,只不過不是返回最終運算值,而是返回一個對象,promise對象,用它來幫你進行異步流程管理。java

先舉個例子幫助理解。Promise對象能夠想象成是工廠生產線上的一個工人,一條生產線由若干個工人組成,每一個工人分工明確,本身作完了把產品傳遞給下一個工人繼續他的工做,以此類推到最後就完成一個成品。這條生產線的組織機制就至關於Promise的機制,每一個工人的工做至關於一個異步函數。後面會繼續拿promise和這個例子進行類比。編程

 

 

Promise風格異步函數的基本寫法:數組

若是用setTimeout來模擬你要進行的異步操做,如下是讓異步函數返回promise的基本寫法。調用Promise構造函數,生成一個promise對象,而後return它。把你的代碼包裹在匿名函數function(resolve, reject){ … } 裏面,做爲參數傳給Promise構造函數。resolve和reject是promise機制內部已經定義好的函數,傳給你用來改變promise對象的狀態。在你的異步代碼結束的時候調用resolve來表示異步操做成功,而且把結果傳給resolve做爲參數,這樣它能夠傳給下一個異步操做。promise

function asyncFn1() {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn1 is done');
            resolve('asyncFn1 value');
        }, 1000);
    });

   return promise;
}

 

在promise機制當中,resolve被調用後會把promise的狀態變成’resolved’。 若是reject被調用,則會把promise的狀態變成’rejected’,表示異步操做失敗。因此在上面的例子中若是你有一些邏輯判斷,能夠在失敗的時候調用reject:異步

//僞代碼
function asyncFn1() {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn1 is done');
            if(success) {
                resolve('asyncFn1 value');
            } else {
                reject('error info');
            }
        }, 1000);
    });

    return promise;
}

 

then()方法:async

既然promise的用來作流程管理的,那確定是多個異步函數要按某種順序執行,而每一個都要return promise對象。怎樣把它們串起來呢?答案是調用promise對象最重要的方法promsie.then(),從它的字面意思就能夠看出它的做用。並且then()方法也返回一個新的promise對象,注意是新的promise對象,而不是返回以前那個。函數

假若有三個異步函數:ui

function asyncFn1() {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn1 is done');
            resolve('asyncFn1 value');
        }, 1000);
    });
    return promise;
}

function asyncFn2(arg) {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn2 is done');
            resolve(arg + ' asyncFn2 value');
        }, 1000);
    });
    return promise;
}

function asyncFn3(arg) {
    var promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            console.log('asyncFn3 is done');
            resolve(arg + ' asyncFn3 value');
        }, 1000);
    });
    return promise;
}

 

能夠用then方法這樣順序來組織它們:

var p1 = asyncFn1(),
    p2 = p1.then(asyncFn2),
    p3 = p2.then(asyncFn3);

p3.then(function(arg) {
    console.log(arg);
});

 

這樣組織起來後,就會按照順序一個一個執行:asyncFn1執行完成後p1變成resolved狀態並調用asyncFn2,asyncFn2運行完後p2變成resolved狀態而且調用asyncFn3,asyncFn3執行完成後p3編程resolved狀態並調用匿名函數打印輸出結果。這個過程當中,若是任何一個promise被變成’rejected’,後續全部promise立刻跟着變成rejected,而不會繼續執行他們所登記的異步函數。

上面代碼能夠更加簡化成這樣,看起來更清爽,用飄柔的感受有沒有:

asyncFn1()
    .then(asyncFn2)
    .then(asyncFn3)
    .then(function(arg) {
        console.log(arg);
    });

 

怎麼樣,比上一節講的回調嵌套代碼漂亮太多啦,多苗條。

如今跟工廠生產線的例子進行類比一下加深理解。你猜上面這段飄柔代碼在工廠生產線例子中至關於什麼?你必定會說,你不是上面說了嘛,至關於一條順序執行的生產線。錯!!! 它至關於---------生產計劃,或者生產圖紙。怕了沒?沒錯就是至關於生產計劃,裏面登記了每一個工人的任務和他們的工做順序。若是把它當成生產線,就會誤覺得asyncFn1()運行完了再調用then,當asyncFn2運行完了再調用下一個then,當asyncFn3運行完了再調用第三個then,這樣會形成是由then來調用這些異步函數的錯覺。實際上then的做用僅僅是登記當每一個promise變成resolved狀態時要調用的下一個函數,僅僅是登記,而不是實際上調用它們,實際調用是發生在promise變成resolved的時候。(then能夠用來登記生產計劃的緣由是它實際上是個同步方法,因此這段飄柔代碼噌得一下就執行完了,計劃就出來了,而不是跟着那些asyncFn函數們一個等一個的執行)。搞清楚這個對於新手來講很是重要,它可讓你更好的來組織你的異步流程。後面會詳細說。另外,工做計劃產生後,生產也同時開始了,即asyncFn函數們也開始執行了,按登記的順序。

 

catch()方法:

上面例子中then方法都是隻接受一個異步函數做爲參數,實際上then方法能夠接受兩個函數做爲參數。第一個函數是Promise對象的狀態變爲Resolved時調用,第二個回調函數是Promise對象 的狀態變爲Rejected時調用。其中,第二個函數是可選的,大部分狀況下不須要提供。可是一種狀況除外就是當你的異步流程結束的時候須要用第二個函數來捕獲異常。即:

asyncFn1()
    .then(asyncFn2)
    .then(asyncFn3)
    .then(null, function(error) {
        console.log(error);
    });

 

最後一步的異常捕獲一般會換一種寫法:

asyncFn1()
    .then(asyncFn2)
    .then(asyncFn3)
    .catch(function(error) {
        console.log(error);
    });

 

catch()是then()用來捕獲異常時的別名或語法糖。它能夠捕獲前面任何promise對象變成rejected的狀態後,所傳遞下來的錯誤信息。若是不使用catch()方法,Promise對象拋出的錯誤就會石沉大海,讓你沒法調試。

 

嵌套promise

Promise機制自己是爲了解決回調嵌套的,但有意思的是promise自己也能夠嵌套,示例以下:

//僞代碼
fn1()
    .then(fn2)
    .then(function(result) {
        return fn3(result)
                .then(fn31)
                .then(fn32)
                .then(fn33);
    })
    .then(fn4)
    .catch(function(err) {
        console.log(err);
    });

 

你怎麼看?我我的觀點,任何事情都沒有絕對的對和錯,好和很差,就是個度的問題。

 

Promise.all()方法:

上一節在回調風格的異步中,最後留了一個思考題,怎樣在循環裏面調用異步函數?如今揭曉答案。

var fs = require('fs');

function foo(dir, callback) {
    fs.readdir(dir, function(err, files) {
        var text = '',
        counter = files.length;
        for(var i=0, j=files.length; i<j; ++i) {
            void function(ii) {
                fs.readFile(files[ii], 'utf8', function(err, data) {
                    text += data;
                    --counter;
                    if(counter===0) {
                        callback(text);
                    }
                });
            } (i);
        }
    });
}

foo('./', function(data) {
    console.log(data);
});

 

上面代碼foo函數讀取當前目錄下全部文件而後合併到一塊兒,由callback把內容傳出來。調用callback的時機也很清楚了,關鍵就是設個計數器(counter),必須當全部readFile回調都完成後再調用callback。順便提一下循環調用異步的時候循環自己必須使用一個匿名函數包裹,爲何?呵呵新手繞不過的坑,答案自行尋找。後面有時間再寫文探討一些javascript的坑坑吧。

怎樣循環回調風格的異步函數如今清楚了,那麼問題來了,怎樣循環promise風格的函數呢?

var fs = require('fs');

//把fs.readdir()改造爲promise風格
function readdirP(dir) {
    return newPromise(function(resolve, reject) {
        fs.readdir(dir, function(err, files) {
            if(err) {
                reject(err);
            } else {
                resolve(files);
            }
        });
    });
}

//把fs.readFile()改造爲promise風格
function readFileP(file) {
    return new Promise(function(resolve, reject) {
        fs.readFile(file, 'utf8', function(err, data) {
            if(err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

function foo(dir) {
    return new Promise(function(resolve, reject) {
        var text = '';
        readdirP(dir).then(function(files) {
            return new Promise(function(resolve, reject) {
                var counter = files.length;
                console.log(counter);
                for(var i=0, j=files.length; i<j; ++i) {
                    void function(ii) {
                        readFileP(files[ii]).then(function(data) {
                            text += data;
                            --counter;
                            if(counter===0) {
                                resolve(text);
                            }
                        });
                    }(i);
                }
            });
        }).then(function(result) {
            resolve(result);
        });
    });
}

foo('./').then(function(data) {
    console.log(data);
});

 

我了個去,怎麼看起來比回調風格的還複雜?沒錯的確是這樣,由於你仍是在用回調思惟寫promise風格的代碼,是個四不像。正宗的寫法應該是這樣的:

function foo(dir) {
    var promise = readdirP(dir)

        .then(function(files) {
            var arr=[];
            for(var i=0, j=files.length; i<j; ++i) {
                arr.push(readFileP(files[i]));
            }
            return Promise.all(arr);
        })

        .then(function(datas) {
            return datas.join('');
        });

    return promise;
}

foo('./').then(function(data) {
    console.log(data);
});

 

這裏關鍵就在於Promise.all()的使用。Promise.all(arr)接受一組promise爲參數,即promise數組。當全部promise都變成resolved的時候就完成了,輸出也是一個數組,即每一個promise所resolve的值。若是任何一個promise變成rejected,則整個失敗,能夠在後面用catch捕獲。標準寫法:

//僞代碼
var arr = [promise1, promise2, promise3];
Promise.all(arr)
    .then(function(resultArr) {
        使用resultArr;
    })
    .catch(function(error) {
        console.log(error);
    });

 

Promise.race()方法:

稍提一下Promise.race(arr)方法,用法跟Promise.all(arr)相似,只不過arr中任何一個promise變resolved/rejected的時候就結束,輸出這個resolve/reject的值。這個方法的功能從它的名字就能夠看出來。

 

最佳實踐:

Promise流程最後必定要加個catch()捕獲可能發生的錯誤。

then(fn)方法只接受函數做爲的參數,fn若是是異步的,則必需要return一個promise對象;若是是同步的,則能夠直接return一個value

function foo(arg) {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            resolve(arg + 1);
        }, 1000);
    });
}


foo(0)
    .then(foo)
    .then(foo)
    .then(function(arg) {
        return arg +1;
    })
    .then(foo)
    .then(function(arg) {
        console.log(arg);
    });

 

猜猜上述代碼最後輸出多少?foo被調用了4次,而且中間有一次同步arg+1的代碼,因此最後輸出5。這裏的同步代碼arg+1太簡單只是爲了演示,若是你的同步代碼比較複雜並且中間可能拋出exception,那最好讓同步代碼也返回一個promise,這樣就能夠在最後catch裏面捕獲到,真是太爽了:

foo(0)
    .then(foo)
    .then(foo)
    .then(function(arg) {
        return Promise.resolve().then(function() {
            return arg +1;
        });
    })
    .then(foo)
    .catch(function(err) {
        console.log(err);
    });

 

即把同步代碼用Promise.resolve().then(function() { … } 進行包裹。Promise.resolve()是生成promise對象的快捷方法,不過它生成的promise對象初始狀態就是resolved的。Promise.resolve()方法還能夠帶參數,這裏不進行詳述,你們能夠自行去了解一下。

用上述方法寫出來的流程,出錯概率會大大減小。

說了這麼久,該說重點了:)

 

堆積木:

返本溯源,promise是爲了解決什麼問題來着?對了,解決回調地獄,本質上是爲了更加清晰的組織異步代碼。Promise的精髓用法就是把一個個異步函數像積木同樣按照它們的順序堆積自來,能夠串行能夠並行,這種堆積木方式的組織流程至關靈活,能夠組織出任意你的業務中須要的流程。這樣說比較抽象,仍是用例子吧:

(這是我實際項目中的一個真實例子)我有5個promise風格的異步函數fn1, fn2, fn3, fn4 和 fn5。fn3須要用到fn2的結果,fn4須要用到fn3的結果, fn5須要用到fn1, fn2, fn3和fn4的結果。是否是挺繞,應該怎麼寫?時間關係就不賣關子了。

var p1 = fn1(),
    p2 = fn2();
    p3 = p2.then(fn3);
    p4 = p3.then(fn4);

var arr = [p1, p2, p3, p4];

Promise.all(arr).then(fn5);

怎麼樣,是否是很神奇?發揮你的想象力,這些異步函數你能夠隨意組合,串行並行。

切記:組合的過程當中每一個異步函數一般只出現一次,除非你業務須要它使用不一樣的數據運行屢次,不然若是出現屢次,極有可能你已經掉坑裏了:

//錯誤代碼
var p1 = fn1(),
    p2 = fn2();
    p3 = fn2().then(fn3);
    p4 = fn2().then(fn3).then(fn4);

var arr = [p1, p2, p3, p4];

Promise.all(arr).then(fn5);

看起來兩組代碼彷佛等價哦,呵呵,只不過錯誤代碼中fn2會跑3次,fn3會跑2次。好好對比清楚:)

我在尚未領悟這種用法的時候是用這樣直腸子的作法:

fn1()
   .then(fn2)
   .then(fn3)
   .then(fn4)
   .then(fn5);

喲?這不是更簡單嗎?錯!由於fn1的輸出在fn2, fn3和fn4中根本沒用,可是仍是必須捎帶在他們每個的輸出結果裏面; fn4根本不須要fn2的輸出,但又要捎帶在fn3裏面以傳給fn4最後給fn5。這樣就形成這些函數深度耦合在一塊兒,功能混亂。 因此記得promise不僅能串行,也能夠並行,就像堆積木同樣很是靈活的進行組合。不知誰這麼聰明發明瞭這種方法:)

 

轉載請註明出處: http://www.cnblogs.com/chrischjh/p/4692743.html 

『本集完』

相關文章
相關標籤/搜索