Promise初級與進階---都在這了

0 前言

我一直覺得我對Promise比較瞭解,相關的方法已經很是熟悉了,直到我看到這篇文章,裏面提出了這樣一個問題:
Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四種 promises 的區別是什麼前端

//1
        doSomething().then(function(){
            return doSomethingElse();
        }).then(finalHandler);
        
        //2
        doSomething().then(function(){
            doSomethingElse();
        }).then(finalHandler);
        
        //3
        doSomething().then(doSomethingElse())
        .then(finalHandler);
        
        //4
        doSomething().then(doSomethingElse)
        .then(finalHandler);

我當時看了是吃驚的,由於我想,這都什麼玩意兒!!!因此我把Promise的方法複習了一遍,而且仔細讀了上面提到的那篇文章,因而就有了這篇文章。es6

在前端開發的學習中,新工具層出不窮,理解當前的基礎是要理解過去,而後瞭解未來。就異步調用而言,ES6中引入Promise簡化異步操做,主要針對的問題就是回調函數的層層嵌套(金字塔問題),除了閱讀不方便以外,只能在當前回調函數函數內部處理異常,這個很難作。Promise經過then和catch方法實現鏈式調用,每一次調用都返回一個Promise對象,擺脫了回調函數層層嵌套的問題和異步代碼「非線性執行」的問題;另外,全部回調函數的報錯均可以經過Promise統一處理,catch能夠捕獲先前調用中全部的異常(冒泡特性)。可是Promise僅僅是對回調作了簡化處理,ES7中的async函數更厲害,結合Promise,徹底不用回調函數,以近似同步的寫法實現異步操做,所須要的僅僅是一個async和await關鍵字而已。本文僅介紹Promise對象,以及ES6中Promise對象具備的一些操做方法。json

1 簡單Promise對象

ES6中原生實現了Promise對象,經過狀態傳遞異步操做的消息。Promise對象有三種狀態:pending(進行中)、resoleved(fulfilled,已完成)、rejected(已失敗),根據異步操做的結果決定處於何種狀態。一旦狀態改變就不可再變,狀態改變僅有兩種pending=>rejected、pending=>resolved。
優勢:避免了層層嵌套的回調函數,並提供了統一的接口,使異步操做更加容易。
缺點:沒法取消Promise;若是不設置回調函數,內部錯誤沒法反映到外部。數組

1.1 建立Promise實例

Promise構造函數接收兩個參數:resolve和reject,這是兩個由JavaScript引擎自動提供的函數,不用本身部署。resolve函數在異步操做成功時調用,做用是將Promise對象的狀態由pending變爲resolved,並將異步操做的結果傳遞出去。reject函數在異步操做失敗時調用,做用是將Promise對象的狀態由pending變爲reject,將異步操做報錯傳遞出去。
then方法能夠接受兩個回調函數做爲參數,第一個是Pormise對象的狀態變爲resolved時調用,另外一個是當Promise對象的狀態變爲rejected時調用,這兩個回調函數都接受Promise對象實例建立過程當中resolve函數和reject函數傳出的值做爲參數。第二個參數可選,事實上通常經過Promise.prototype.catch()調用發生錯誤時的回調函數,經過then調用異步操做成功時的回調函數。
實例1:promise

//返回Promise對象,setTimeout中傳遞的resolve參數爲’done’
        function timeout(ms) {
          return new Promise((resolve, reject) => {
            setTimeout(resolve, ms, 'done');
          });
        }
        
        timeout(100).then((value) => {
          console.log(value);
        }); //done

實例2:Promise執行流異步

//建立Promise實例
        let promise = new Promise(function(resolve, reject) {
          console.log('Promise');//當即執行
          resolve();
        });
        //resolved狀態調用在當前腳本全部同步任務執行完纔會執行
        promise.then(function() {
          console.log('Resolved.');
        });
        //當即執行
        console.log('Hi!');

對以上代碼,Promise新建後當即執行,因此首先輸出的是「Promise」。而後,then方法指定的回調函數,將在當前腳本全部同步任務執行完纔會執行,因此「Resolved」最後輸出。async

reject函數在異步操做失敗時調用,所以參數經常是一個錯誤對象(Error Object);resolve函數在操做成功時調用,參數經常是正常值或者另外一個Promise實例,這表徵了異步嵌套,異步嵌套的外層Promise要等待內層Promise的狀態決定下一步狀態。函數

var p1 = new Promise(function (resolve, reject) {
          setTimeout(() => reject(new Error('fail')), 3000)
        })
        
        var p2 = new Promise(function (resolve, reject) {
          setTimeout(() => resolve(p1), 1000)
        })

        p2.then(result => console.log(result)) //p1 is rejected, p2 is the same as p1
          .catch(error => console.log(error)) // Error: fail

因爲p2的resolve方法將p1做爲參數,p1的狀態決定了p2的狀態,若是p1的狀態是pending,p2的回調函數會等待p1的狀態改變;若是p1的狀態是resolved或rejected,p2的回調函數當即執行。p2的狀態在1秒以後改變,resolve方法返回的是p1。此時,因爲p2返回的是另外一個Promise,因此後面的then語句都變成針對後者(p1)。又過了2秒,p1變爲rejected,致使觸發catch方法指定的回調函數。工具

1.2 Promise.prototype.then()

then方法爲Promise實例添加狀態改變時的回調函數,返回一個新的Promise實例,能夠採用鏈式寫法,前一個then方法的返回值做爲後一個then方法的參數:oop

getJSON("/posts.json").then(function(json) {  // json comes from 「/posts.json」
          return json.post;
         }).then(function(post) {  //post comes from json.post
          // ...
        });

若是第一個then方法內的回調函數返回一個Promise對象,後續的then方法會根據這個新的Promise對象的狀態執行回調函數。

getJSON("/post/1.json").then(function(post) {
          return getJSON(post.commentURL);
        }).then(function funcA(comments) {
          console.log("Resolved: ", comments);
        }, function funcB(err){
          console.log("Rejected: ", err);
        });

第一個then方法指定的回調函數,返回的是另外一個Promise對象。這時,第二個then方法指定的回調函數,就會等待這個新的Promise對象狀態發生變化。若是變爲Resolved,就調用funcA,若是狀態變爲Rejected,就調用funcB。回調函數通常是匿名函數,上述僅僅是爲了便於理解寫成命名函數

1.3 Promise.prototype.catch()

catch(rejection)方法是then(null,rejection)的別稱,僅僅當發生錯誤時執行,catch的存在是將錯誤回調函數從then()方法中剝離出來。

getJSON('/posts.json').then(function(posts) {
          // ...
        }).catch(function(error) {
          // 處理 getJSON 和 前一個回調函數運行時發生的錯誤
          console.log('發生錯誤!', error);
        });

Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。

getJSON('/post/1.json').then(function(post) {
          return getJSON(post.commentURL);
        }).then(function(comments) {
          // some code
        }).catch(function(error) {
          // 處理前面三個Promise產生的錯誤
        });

catch方法返回的仍是一個 Promise 對象,所以後面還能夠接着調用then方法。要是後續then方法裏面報錯,就與前面的catch無關了。若是最後一個catch方法內部拋出錯誤,是沒法捕獲的。爲了不潛在錯誤,最好是在最後用一個catch方法兜底。

1.4 Promise.all():偏重狀態改變的邏輯與關係

用於將多個Promise實例包裝成一個新的Promise實例。若是內部參數不是Promise實例,就調用Promise.resolve()將參數轉換爲Promise實例。

var p = Promise.all([p1, p2, p3]);//接受一個Promise數組做爲參數

p的狀態由p一、p二、p3決定,呈現&關係,fulfilled對應1,rejected對應0,分紅兩種狀況:
(1)只有p一、p二、p3的狀態都變成fulfilled,p的狀態纔會變成fulfilled,此時p一、p二、p3的返回值組成一個數組,傳遞給p的回調函數。
(2)只要p一、p二、p3之中有一個被rejected,p的狀態就變成rejected,此時第一個被reject的實例的返回值(rejected的順序有沒有相似與操做的順序?),會傳遞給p的回調函數。

const databasePromise = connectDatabase();
        const booksPromise = databasePromise
          .then(findAllBooks);
        
        const userPromise = databasePromise
          .then(getCurrentUser);
        
        Promise.all([
          booksPromise,
          userPromise
        ])
        .then(([books, user]) => pickTopRecommentations(books, user));

上面代碼中,booksPromise和userPromise是兩個並行執行的異步操做,只有等到它們的結果都返回了,纔會觸發pickTopRecommentations這個回調函數。

1.5 Promise.race():偏重狀態改變的時間順序

Promise.race()和Promise.all()一樣是將多個Promise實例包裝成一個新的Promise實例,可是隻要實例數組中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給新實例的回調函數。

var p = Promise.race([p1, p2, p3]);

Promise.race方法的參數與Promise.all方法同樣,若是不是 Promise 實例,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。

const p = Promise.race([
          fetch('/resource-that-may-take-a-while'),
          new Promise(function (resolve, reject) {
            setTimeout(() => reject(new Error('request timeout')), 5000)
          })
        ]);
        p.then(response => console.log(response));
        p.catch(error => console.log(error));

上面代碼中,若是5秒以內fetch方法沒法返回結果,變量p的狀態就會變爲rejected,從而觸發catch方法指定的回調函數。

1.6 Promise.resolve()

將現有對象轉化爲Promise對象,根據參數不一樣有不一樣結果:
(1) 參數是一個Promise實例,Promise.resolve()將原對象返回;
(2) 參數是具備then方法的對象,Promise.resolve方法會將這個對象轉爲Promise對象,而後就當即執行thenable對象的then方法。

let thenable = {
          then: function(resolve, reject) {
            resolve(42);
          }
        };
        
        let p1 = Promise.resolve(thenable);
        p1.then(function(value) {
          console.log(value);  // 42
        });

thenable對象的then方法執行後,對象p1的狀態就變爲resolved,從而當即執行最後那個then方法指定的回調函數,輸出42。

(3) 若是參數是一個原始值(基本類型值),或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態爲Resolved。

var p = Promise.resolve('Hello');
        p.then(function (s){
          console.log(s)
        });
        // Hello

上面代碼生成一個新的Promise對象的實例p。因爲字符串Hello不屬於異步操做(判斷方法是它不是具備then方法的對象),返回Promise實例的狀態從一輩子成就是Resolved,因此回調函數會當即執行。Promise.resolve方法的參數,會同時傳給回調函數。
(4) Promise.resolve方法容許調用時不帶參數,直接返回一個Resolved狀態的Promise對象。因此,若是但願獲得一個Promise對象,比較方便的方法就是直接調用Promise.resolve方法。

var p = Promise.resolve();
        p.then(function () {
          // ...
        });

當即resolve的Promise對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時,這個很好理解,經過resolve產生的Promise對象而後調用then函數和先產生Promise對象,對象轉換成resolved後再執行then函數是同樣的,都是在本輪事件輪詢的末尾執行。

//下一輪事件輪詢開始
        setTimeout(function () {
          console.log('three');
        }, 0);
        //本輪事件輪詢末尾
        Promise.resolve().then(function () {
          console.log('two');
        });
        //當即執行
        console.log('one');
        
        // one two three

上面代碼中,setTimeout(fn, 0)在下一輪「事件循環」開始時執行,Promise.resolve()在本輪「事件循環」結束時執行,console.log(’one‘)則是當即執行,所以最早輸出。

1.7 Promise.reject()

Promise.reject()返回一個Promise實例,實例狀態爲rejected。Promise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。
實例1:

var p = Promise.reject('出錯了');
        // 等同於var p = new Promise((resolve, reject) => reject('出錯了'))
        //參數就是’出錯了’
        p.then(null, function (s) {
          console.log(s)
        });// 出錯了

實例2:

const thenable = {
          then(resolve, reject) {
            reject('出錯了');
          }
        };
        //參數就是thenable
        Promise.reject(thenable)
        .catch(e => {
          console.log(e === thenable)
        })
        // true

上面代碼中,Promise.reject方法的參數是一個thenable對象,執行之後,後面catch方法的參數不是reject拋出的「出錯了」這個字符串,而是thenable對象。

2 加深Promise理解

Promises 給予咱們的就是在咱們使用異步Callback時丟失的最重要的語言基石: return, throw 以及堆棧。可是想要 promises 可以提供這些便利給你的前提是你知道如何正確的使用它們。

2.1 使用Promise.resolve()建立Promise對象

任何有可能 throw 同步異常的代碼都是一個後續會致使幾乎沒法調試異常的潛在因素。可是若是你將全部代碼都使用Promise.resolve() 封裝,那麼你老是能夠在以後使用 catch() 來捕獲它。所以方法2要優於方法1。

方法1:

new Promise(function(resolve,reject){
    resolve(someSynchronousValue);
}).then(/*-------------*/);

方法2:

function somePromiseAPI() {
    return Promise.resolve().then(function(){
    doSomethinThatMayThrow();
    return ‘foo’;
}).then(/*------------*/);
}

2.2 catch() 與 then(null, ...) 根據使用場景並不是徹底等價

如下代碼等價:

somePromise().catch(function (err)){
    //handle error
    });
    //////////////////////////////////////
    somePromise().then(null, function(err)) {
    //handle error
    }

可是如下代碼不等價:

somePromise().then(function(){
        return someOtherPromise();
    }).catch(function(err){
        //error
    });
    ///////////////////////////////
    somePromise().then(function(){
        return someOtherPromise();
    },function(err){
        //error
    });

所以,當你使用 then(resolveHandler, rejectHandler) 這種形式時,rejectHandler 並不會捕獲由 resolveHandler 引起的異常。最好不使用then()的第二個參數,而是老是使用catch(),惟一例外是寫一些異步的Mocha測試時,使用then()的第二個參數,但願拋出用例的異常。

2.3 promises vs promises factories

當咱們但願執行一個個的執行一個 promises 序列,即相似 Promise.all() 可是並不是並行的執行全部 promises。你可能天真的寫下這樣的代碼:

function executeSequentially(promise){
            var result = Promise.resolve();
            promises.forEach(function (promise)){
                result = result.then(promise);
            });
            return result;
        }

不幸的是,這份代碼不會按照你的指望去執行,你傳入 executeSequentially() 的 promises 依然會並行執行。其根源在於你所但願的,實際上根本不是去執行一個 promises 序列。依照 promises 規範,一旦一個 promise 被建立,它就被執行了。所以你實際上須要的是一個 promise factories 數組。
我知道你在想什麼:「這是哪一個見鬼的 Java 程序猿,他爲啥在說 factories?」 。實際上,一個 promises factory 是十分簡單的,它僅僅是一個能夠返回 promise 的函數:

function executeSequentially(promiseFactories){
            var result = Promise.resolve();
            promiseFactories,forEach(function (promiseFactory){
            result = result.then(promiseFactory)
        });
        return result;
        }
        function promiseFactory(){
            return somethingThatCreatesAPromise();
        }

爲什麼這樣就能夠了?這是由於一個 promise factory 在被執行以前並不會建立 promise。它就像一個 then 函數同樣,而實際上,它們就是徹底同樣的東西。若是你查看上面的 executeSequentially() 函數,而後想象 myPromiseFactory 被包裹在 result.then(...) 之中,也許你腦中的小燈泡就會亮起。在此時此刻,對於 promise 你就算是悟道了。

2.4 promises 穿透

Promise.resolve(‘foo’).then(Promise.resolve(‘bar’)).then(function(result){
        console.log(result);
        });

執行結果並不是是bar,而是foo,這是由於當then()接受非函數的參數時,會解釋爲then(null),這就致使前一個Promise的結果穿透到下面一個Promise。正確的寫法是在then()方法內部包含函數:

Promise.resolve(‘foo’).then(function(){
        return Promise.resolve(‘bar’);
        }).then(function(result){
        console.log(result);
        });

2.5 Promise.all()

Promise.all()以一個Promise數組做爲輸入,返回一個新的Promise,特色在於它會並行執行數組中的每一個Promise,而且每一個Promise都返回後才返回結果數組,這就數組的異步版map/forEach方法。可是若是須要返回兩個不相關的結果,使用Promise.all()能夠產生兩個不相關的數組結果;可是若是後一結果要依靠前一個結果產生,此時在Promise裏使用嵌套也就能夠的:

getUserByName(‘bill’).then(function(user){
            return getUserAccountById(user.id);
        }).then(function (userAccount){
        /*-----------------*/
        });

或者在內部使用嵌套:

getUserByName(‘bill’).then(function(user){
                return getUserAccountById(user.id).then(function(userAccount){
                    /*--------------------*/
                });
        });

忘記使用catch:沒人能夠保證不出錯,因此仍是在最後加一個catch吧!

Q: 假定 doSomething() 和 doSomethingElse() 均返回 promises,下面的四種 promises 的區別是什麼

//1
        doSomething().then(function(){
            return doSomethingElse();
        }).then(finalHandler);
        // doSomething()返回一個Promise實例,可是後續的then方法裏是一個匿名函數,該函數產生一個新的Promise實例並返回這個實例,所以finalHandler的參數就是這個實例的resolve返回值。
        doSomething
        /-----------------/
                    doSomethingElse(undefined)
                    /----------------------------------/
                                            finalHandler(reulstOfDoSomethingElse)
                                            /--------------------------------------------------/
        //2
        doSomething().then(function(){
            doSomethingElse();
        }).then(finalHandler);
        //doSomething()返回一個Promise實例,可是後續的then方法裏是一個匿名函數,該函數產生一個新的Promise實例,可是因爲這個函數沒有返回值,所以finalHandler函數沒有參數。
        doSomething
        /-----------------/
                    doSomethingElse(undefined)
                    /----------------------------------/
                    finalHandler(undefined)
                    /----------------------------------/
        
        //3
        doSomething().then(doSomethingElse())
        .then(finalHandler);
        // doSomething()返回一個Promise實例,可是後續的doSomethingElse()是一個當即執行函數,不接受上一Promise實例的resolve參數,因此參數是undefined,這時doSomething()的Promise穿透到finalHandler,finalHandler的參數就是該Promise的resolve參數。
        doSomething
        /-----------------/
        doSomethingElse(undefined)
        /----------------------------------/
                                finalHandler(reulstOfDoSomething)
                                /--------------------------------------------------/
        
        //4
        doSomething().then(doSomethingElse)
        .then(finalHandler);
        //doSomething()返回一個Promise實例,隨後調用doSomethingElse以上一實例的resolve參數產生第二個Promise,最後是finalHandler以上一實例的resolve參數產生第三個Promise。
        doSomething
        /-----------------/
                    doSomethingElse(resultOfDoSomething)
                    /----------------------------------/
                                            finalHandler(reulstOfDoSomethingElse)
                                            /--------------------------------------------------/
相關文章
相關標籤/搜索