[es6系列]學習Promise

本文是基於對阮一峯的Promise文章的學習整理筆記,整理了文章的順序、增長了更多的例子,使其更好理解。javascript

1. 概述

在Promise以前,在js中的異步編程都是採用回調函數和事件的方式,可是這種編程方式在處理複雜業務的狀況下,很容易出現callback hell(回調地獄),使得代碼很難被理解和維護。java

Promise就是改善這種情形的異步編程的解決方案,它由社區最先提出和實現,es6將其寫進了語言標準,統一了用法,而且提供了一個原生的對象Promisenode

2. 理解Promise

咱們經過一個簡單例子先來感覺一下Promise。git

var p = new Promise(function (resolve, reject) {
    // ...
    if(/* 異步操做成功 */){
        resolve(ret);
    } else {
        reject(error);
    }
});

p.then(function (value) {
    // 完成態
}, function (error) {
    // 失敗態
});

咱們須要關注的是es6

  • Promise的構造函數github

  • resolve() , reject()chrome

  • then()編程

2.1 Promise構造函數

咱們在經過Promise構造函數實例化一個對象時,會傳遞一個函數做爲參數,那麼這個函數有什麼特色?數組

答案就是在新建一個Promise後,這個函數會當即執行。promise

let promise = new Promise(function (reslove, reject) {
    console.log('Promise');
});

console.log('end');

執行結果以下:

圖片描述

能夠看到是先輸出了Promise,再輸出了end

2.2 resolve/reject

在Promise中,對一個異步操做作出了抽象的定義,Promise操做只會處在3種狀態的一種,他們之間的轉化如圖所示

圖片描述

注意,這種狀態的改變只會出現從未完成態向完成態或失敗態轉化,不能逆反。完成態和失敗態不能互相轉化,並且,狀態一旦轉化,將不能更改。

只有異步操做的結果能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思是承諾,表示其餘手段沒法改變。

在聲明一個Promise對象實例時,咱們傳入的匿名函數參數中:

  • resolve就對應着完成態以後的操做

  • reject對應着失敗態以後的操做

2.3 then()

那麼問題來了,then()方法有什麼做用?resolve和reject又是從哪裏傳遞過來的?

其實這兩個問題是一個問題,在實例化一個Promise對象以後,咱們調用該對象實例的then()方法傳遞的兩個參數中:

  • 第一個參數(函數)對應着完成態的操做,也就是resolve

  • 第二個參數(函數)對應着失敗態的操做,也就是reject

那就是說,在Promise中是經過then()方法來指定處理異步操做結果的方法。

2.4 實際案例

到這裏咱們明白了Promise的語法,也瞭解了Promise中函數是如何執行的,結合一個實際的案例,來加深對Promise的理解。

咱們來實現一個異步加載圖片的函數

function loadImageAsync(url) {
    return new Promise(function (reslove, reject) {
        var img = new Image();
        img.onload = function () {
            reslove();
        }
        img.onerror = function () {
            reject();
        }
        console.log("loading image");
        img.src = url;
    });
}
var loadImage1 = loadImageAsync("https://www.google.co.jp/images/branding/googlelogo/2x/googlelogo_color_272x92dp.png");
loadImage1.then(function success() {
    console.log("success");
}, function fail() {
    console.log("fail");
});

var loadImage2 = loadImageAsync("1.png");
loadImage2.then(function success() {
    console.log("success");
}, function fail() {
    console.log("fail");
});

咱們在chrome中執行,先是傳遞一個有效的url,再傳遞一個無效的url,執行的效果爲:

圖片描述

3. Promise進階

3.1 resolve/reject的參數

reject函數的參數通常來講是Error對象的實例,而resolve函數的參數除了正常的值外,還多是另外一個Promise實例,表示異步操做的結果有多是一個值,也有多是另外一個異步操做。

var p1 = new Promise( function(resolve, reject) {
    // ...
});

var p2 = new Promise( function(resolve, reject) {
    // ...
    resolve(p1);
});

代碼分析:p1和p2都是Promise的實例,p2中的resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做。

注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態,他們之間的關係是

圖片描述

舉個例子

console.time('Promise example start')
var p1 = new Promise( (resolve, reject) => {
    setTimeout(() => resolve('hi'), 3000);
});

var p2 = new Promise( (resolve, reject) => {
    setTimeout(() => resolve(p1), 10);
});

p2.then( ret => {
    console.log(ret);
    console.timeEnd('Promise example end')
});

咱們在node環境下運行以上代碼,執行結果爲:

圖片描述

從執行時間能夠看到,p2會等待p1的執行結果,而後再執行,從輸出hi能夠看到p1完成狀態轉變以後,傳遞給resolve(或者reject)的結果會傳遞給p2中的resolve

3.2 then()

從上面的例子,咱們能夠了解到then()方法是Promise實例的方法,即Promise.prototype上的,它的做用是爲Promise實例添加狀態改變時的回調函數,這個方法的第一個參數是resolved狀態的回調函數,第二個參數(可選)是rejected狀態的回調函數。

那麼then()方法的返回值是什麼?then方法會返回一個新的Promise實例(注意,不是原來那個Promise,原來那個Promise已經承諾過,此時繼續then就須要新的承諾~~),這樣的設計的好處就是可使用鏈式寫法。

還有一個點,就是鏈式中的then方法(第二個開始),它們的resolve中的參數是什麼?答案就是前一個then()中resolve的return語句的返回值。

來一個示例:

var p1 = new Promise( (resolve, reject) => {
    setTimeout(() => resolve('p1'), 10);
});

p1.then( ret => {
    console.log(ret);
    return 'then1';
}).then( ret => {
    console.log(ret);
    return 'then2';
}).then( ret => {
    console.log(ret);
});

在node環境下執行,執行結果爲:

圖片描述

3.3 catch()錯誤處理

catch()方法是Promise實例的方法,即Promise.prototype上的屬性,它實際上是.then(null, rejection)的簡寫,用於指定發生錯誤時的回調。

這個方法其實很簡單,在這裏並不想討論它的使用,而是想討論的是Promise中的錯誤的捕抓和處理。

3.3.1 Error對象的傳遞性

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

var p = new Promise( (resolve, reject) => {
    setTimeout(() => resolve('p1'), 10);
});

p.then( ret => {
    console.log(ret);
    throw new Error('then1');
    return 'then1';
}).then( ret => {
    console.log(ret);
    throw new Error('then2');
    return 'then2';
}).catch( err => {
    // 能夠捕抓到前面的出現的錯誤。
    console.log(err.toString());
});

執行結果以下

圖片描述

在第一個then中拋出了一個錯誤,在最後一個Promise對象中能夠catch到這個錯誤。

由於有這種方便的錯誤處理機制,因此通常來講不要在then方法裏面定義reject狀態的回調函數, 而是使用catch方法

3.3.2 vs try/catch

跟傳統的try/catch不一樣的是,若是沒有使用catch方法指定錯誤處理回調函數,則Promise對象拋出的錯誤不會傳遞到外層代碼(在chrome會報錯)

Node.js有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤。如下代碼就是在node環境下運行。

var p = new Promise((resolve, reject) => {
    resolve(x + 2);
});
p.then( () => {
    console.log('nothing');
});

圖片描述

3.3.3 catch()的返回值

沒錯,既然catch()是.then(null, rejection)的別名,那麼catch()就會返回一個Promise對象,所以在後面還能夠接着調用then方法,示例代碼以下:

var p = new Promise((resolve, reject) => {
    resolve(x + 2);
});
p.then( () => {
    console.log('nothing');
}).catch( err => {
    console.log(err.toString());
    return 'catch';
}).then( ret => {
    console.log(ret);
});

圖片描述

當出錯時,catch會先處理以前的錯誤,而後經過return語句,將值繼續傳遞給後一個then方法中。

若是沒有報錯,則跳過catch,示例以下:

var p = new Promise((resolve, reject) => {
    resolve('p');
});
p.then( ret => {
    console.log(ret);
    return 'then1';
}).catch( err => {
    console.log(err.toString());
    return 'catch';
}).then( ret => {
    console.log(ret);
});

圖片描述

4. Promise對象方法

4.1 Promise.all()

Promise.all()方法用於將多個Promise實例,包裝成一個新的Promise實例,例如

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

新的Promise實例p的狀態由p1, p2, p3決定:

  • p1, p2, p3的狀態都爲完成態時,p爲完成態。

  • p1, p2, p3中任一一個狀態爲失敗態,則p爲失敗態。

4.2 Promise.race()

Promise.race方法一樣是將多個Promise實例,包裝成一個新的Promise實例。

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

不一樣的是,只要p1, p2, p3中任意一個實例率先改變狀態,則p的狀態就跟着改變,並且狀態由率先改變的實例決定。

var p = Promise.race([
    new Promise(resolve => {
        setTimeout(() => resolve('p1'), 10000);
    }),
    new Promise((resolve, reject) => {
        setTimeout(() => reject(new Error('time out')), 10);
    })
]);
p.then( ret => console.log(ret))
    .catch( err => console.log(err.toString()));

圖片描述

4.3 Promise.resolve()

Promise.resolve()能夠將現有的對象轉爲Promise對象。

var p = Promise.resolve('p');

// 至關於
var p = new Promise(resolve => resolve('p'));

比較有意思的是Promise.resolve()會根據參數類型進行相應的處理,分幾種狀況討論。

4.3.1 Promise實例

參數是一個Promise實例,那麼Promise.resolve將不作任何處理,直接返回這個實例。

4.3.2 thenable對象

參數是一個thenable對象,也就是說對象是具備then方法的對象,但不是一個Promise實例(就跟類數組和數組的關係同樣),例如

let thenable = {
    then : function (resolve, reject) {
        resolve(42);
    }
};

let p = Promise.resolve(thenable);
p.then( ret => console.log(ret)); // 42

Promise.resolve方法會將這個對象轉爲Promise對象,而後當即執行thenable對象中的then方法,由於例子中的thenable對象的then方法中執行了resolve,所以會輸出結果42

4.3.3 其餘參數

若是參數是一個原始值,或者不具備then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態爲resolve,而後直接將該參數傳遞給resolve方法。

var p = Promise.resolve("p");
p.then( ret => console.log(ret)); // p

4.3.4 不帶任何參數

Promise.resolve方法不帶參數時,會直接返回一個resolve狀態的Promise對象。

須要注意的當即resolve的Promise對象,是在本輪事件循環的結束時,而不是下一輪事件循環的開始執行。示例代碼:

setTimeout(() => console.log('3'), 0);
var p = Promise.resolve();
p.then(() => console.log('2'));
console.log('1');

輸出結果爲:

圖片描述

4.4 Promise.reject()

Promise.reject()返回一個新的Promise實例,該實例的狀態爲rejected,對於傳入的參數的處理跟Promise.resolve相似,就是狀態都爲rejected

5. 兩個實用的方法

5.1 done()

Promise對象的回調鏈,無論以then方法或者catch方法結尾,要是最後一個方法拋出錯誤,都有可能沒法捕捉到,由於Promise內部的錯誤不會冒泡到全局,所以,咱們能夠提供一個done方法,老是處理回調鏈的尾端,保證拋出任何可能出現的錯誤。

這個代碼的實現很是簡單

Promise.prototype.done = function (resolve, reject) {
    this.then(resolve, reject)
        .catch( function (reason) {
            // 拋出一個全局錯誤
            setTimeout( () => { throw reason }, 0);
        });
}

// 使用示例
var p = new Promise( (resolve, reject) => {
    resolve('p');
});
p.then(ret => {
    console.log(ret);
    return 'then1';
}).catch( err => {
    console.log(err.toString());
}).then( ret => {
    console.log(ret);
    return 'then2';
}).then( ret => {
    console.log(ret);
    x + 2;
}).done();

圖片描述

這裏爲何能夠在全局拋出一個錯誤?緣由就是setTimeout中的回調函數是在全局做用域中執行的,所以拋出的錯誤就是在全局做用域上。

5.2 finally()

finally方法用於指定無論Promise對象最後的狀態如何,都會執行的操做,它與done方法最大的區別就是,它接受一個普通函數做爲參數,該函數無論怎麼樣都必須執行。

Promise.prototype.finally = function (callback) {
    let P = this.constructor;
    return this.then(
        ret => P.resolve(callback()).then( () => ret),
        err => P.resolve(callback()).then( () => {throw reason })
    );
};

5. Promise的優劣勢

從上面幾個小節綜合來看,能夠看到Promise其實就是作了一件事情,那就是對異步操做進行了封裝,而後能夠將異步操做以同步的流程表達出來,避免了層層嵌套的回調函數,同時提供統一的接口,使得控制異步操做更加容易。

可是,Promise也有一些缺點:

  • 沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。

  • 若是不設置回調函數,Promise內部的錯誤不會反應到外部。

  • 當處於未完成態時,沒法得知目前進展到哪個階段。

6. Promise與generator的結合

使用Generator函數來管理流程,遇到異步操做的時候,一般返回一個Promise對象。

function getFoo() {
    return new Promise( resolve => resolve('foo'));
}

var g = function * () {
    try {
        var foo = yield getFoo();
        console.log(foo);
    } catch(e){}
}

function run(generator) {
    var it = generator();

    function go(result) {
        if(result.done) return result.value;

        // 默認value是一個Promise,其實這裏應該作判斷的
        if(!(result.value instanceof Promise)){
            throw Error('yield must follow an instanceof Promise');
        }
        return result.value.then(
            ret => go(it.next(ret))
        ).catch(err => go(it.throw(err)));
    }

    go(it.next());
}

run(g);

上面代碼的Generator函數g之中,有一個異步操做getFoo,它返回的就是一個Promise對象。函數run用來處理這個Promise對象,並調用下一個next方法。

7. 來源

我的博客

相關文章
相關標籤/搜索