Promise 基礎學習

Promise 是ES6的特性之一,採用的是 Promise/A++ 規範,它抽象了異步處理的模式,是一個在JavaScript中實現異步執行的對象。
按照字面釋意 Promise 具備「承諾」的含義,它承諾當異步處理完成後,回饋一個結果給你!或者你能夠將其認爲是一個狀態機,一旦狀態發生了改變,便會觸發對應的行爲。編程

Promise 最先出現於E語言中(一種給予並列/並行處理設計的編程語言),JavaScript 引入這一特性,旨在爲了規範異步的操做和避免陷入回調地獄。數組


目錄promise

  • 如何使用?
  • Promise的狀態
  • Promise的簡單示例
  • 鏈式調用&&值的傳遞
  • 鏈式調用&&狀態處理
  • Promise.all
  • Promise.race
  • Promise的同步調用
  • Promise && callback
  • $.Deferred
  • 初探Promise的基本實現
  • Promise的常見問題

如何使用?

Promise的使用主要有兩種方式,一種是對象實例化操做,它具備固定的使用格式:dom

new Promise(exector);

具體示例:異步

var promise = new Promise(function(resolve,reject){
    if(success){
        resolve();
    }else{
        reject();
    }
})

exector 是一個做爲參數的匿名函數,它接收兩個參數,一個是 resolve ,另外一個則是 reject,這兩個參數都是方法,經過執行兩個方法咱們能夠修改 Promise 實例對象的狀態,使其再觸發對應的行爲。編程語言

另外一種則是靜態調用,這些方法自己就是 Promise 對象的靜態實現:函數

Promise.resolve().then(resolve);
Promise.reject().then(undefined,rejected);
Promise.reject().catch(rejected);

靜態調用經常使用於快速執行一個異步操做,例如在咱們的程序功能中有一個耗時很長的循環,這個循環的目的只是爲了計算一個結果並顯示,可是若直接放在程序的上下文的地方,會致使阻塞,經常使用的方式是將其加入到一個定時器中進行異步操做:工具

setTimeout(function(){
    for(;;){;}
},16);

可是學習了靜態調用 Promise,咱們徹底能夠將這個操做放入到要給 Promise的異步回調中。oop

Promise.resolve().then(function(){

    for (var i = 0; i < 100000; i++) {
        if(i  === 100000/2){
            console.log('loop end2');
        }
    }
});

對比這兩種方式,不難發現經過對象實例的方式,咱們能夠爲實例對象賦予更多的功能,能夠根據自身的須要手動的改變 Promise 的狀態,而使用靜態調用的方式,則能夠快速的進行異步操做。
須要注意的是,不管是靜態的方式仍是實例化對象,根據Promise的狀態被調用的方法都是以異步方式執行的。可是 Promise 對象實例化的過程卻依然是同步的。學習

Promise的狀態

Promise 有三種狀態:penddingrejectedfulfilled;

  • pendding : 表示初始化狀態
  • rejected : 表示失敗狀態
  • fulfilled: 表示成功完成狀態

而狀態的變化,則須要經過執行對應的方法來完成,

  • pendding -> fulfilled 經過 resolve() 方法來完成。
  • pendding -> rejected : 經過 reject() 方法來完成。

Promise 默認的狀態是 pendding 狀態,這種狀態出如今實例對象剛剛初始化的狀況,結束於 resolve() 或者是 reject() 方法調用以後。一旦狀態發生改變,便沒法再修改,也所以說明,狀態改變後執行的回調操做 then 也只會執行一次。除此以外,在 Promise 中主動使用 throw new Error() 也可使 promise的狀態改變爲 rejected

Promise的簡單示例

Promise 的實例對象一旦建立好後,會大體具備如下的操做:

  • 初始化狀態爲pendding
  • 附加 thencatch 等異步處理方法
  • 執行 exector 方法(同步的方式),根據具體的行爲來決定是否變動 promise實例對象的狀態。

實際上經過靜態調用的方式來執行 Promsie,除了不具備 exector 其它的都是相同的。
須要着重說明的是 thencatch 這兩個方法,它們都是 Promise對象的狀態回調函數,一旦 promise的狀態發生改變,便會對應的進行觸發。

var promise = new Promise(function(resolve, reject) {
    var num = Math.random() * 5;
    setTimeout(function() {
        if (num >= 2.5) {
            reject('數值過大');
        } else {
            resolve(num);
        }
    }, 1000)
});


promise.then(function(v) {
    console.log('success:' + v);
}, function(v) {
    console.log(v)
});

then 方法有兩個參數:promise.then(onResolved,onRejected),其中 onResolved 表示成功(狀態變動爲 fulfilled)的回調函數,而 onRejected 則表示失敗(狀態變動爲 rejected)狀況下的回調函數,通常來講第二個參數能夠忽略不寫,只保留成功的回調方法 then(onResolved),可是若是你只想處理失敗的回調函數,那麼 onResolved 並不能被省去,promise.then(undefined,onRejected)

或者將 rejected 的處理單獨提取出來是更好的辦法:

promise.then(function(v) { console.log('success:' + v); }).catch(function(v) { console.log(v) });

Promsie 支持這種相似JQ的鏈式調用,而且能夠同時連續調用多個 then 方法,而這裏的 catch 方法與咱們的 try..catch 功能相同,都是用於捕獲錯誤。並且還能夠將錯誤單獨的提取出來,這便爲咱們帶來一個很是大的優點那就是哪怕我then方法中的 onResolved 方法執行錯誤,也不會阻塞其它代碼的執行。

而可以以鏈式連續屢次執行 then 方法的緣由就在於咱們調用 then 方法的時候,該方法會返回一個新的 promise 對象,一樣的,對於 catch 方法而言道理也是相同的,只是 catch 不能作到屢次調用。
經過這個簡單的實例,咱們可知一個promise實例對象的執行是同步的,可是根據狀態改變的句柄方法是異步執行的,同時這些句柄方法還會再次返回一個新的 Promise對象,用於進行鏈式調用。

鏈式調用&&值的傳遞

Promsie實例對象中的 then或者是 catch 方法不只能夠接受 exector 中經過 resolve(value) 或者是 reject(value) 傳遞而來的值,還能夠在其回調函數中經過 return 語句將值傳遞給調用鏈的下一個 then 或者是 catch 方法。

Promise.resolve(1).then(function(v){return v+1}).then(function(v){return v+1}).then(function(v){console.log(v)}) // 3

實際上當 then 方法執行完成後,會返回一個新的 Promise 對象,而且將本身接收的值附加到這個promise對象上做爲一個參數值,供調用鏈上的下一個 then或者是catch方法讀取並處理。

若是去詳細的討論 resolve(value) 或者是 reject(value)值的傳輸的話,它主要有如下幾種狀況:

  • resolve(promsieObj) : 若是接收的是一個promise對象做爲參數,則返回的promise對象即是這個做爲參數的promsie對象。
  • resolve(like-promise):若是接收的參數是一個類promise對象,則將其轉換並返回(帶有then)一個新的promsie對象。
  • resolve(value):若是參數只是一個普通的js數據類型值,則返回一個新的promise對象,而且該promise對象的值就是這個參數。
Promise.resolve(Promise.resolve(3)).then(function(v){console.log(v)});
Promise.resolve('123').then(function(v){console.log(v)})

resolvereject 基本相同,不一樣的只是,若是reject接收到的是另外一個promise做爲參數,則返回的並非新的promise對象,依然是其自己。

鏈式調用&&狀態處理

與ES3中咱們會用 try..catch..finally 來進行異常的處理,那麼在Promise中,異常都會有何種的流程呢?

Promise.reject(1).then(function(v) {
    console.log('success:' + v);
    return v
}).catch(function(v) {
    console.log('error:' + v);
        return v
}).then(function(v) {
    console.log(v);
    return v;
}).then(function(v) {
    console.log(v)
});

/*
 * error:1
 * 1
 * 1
 * /

從運算的結果上咱們能夠看出,then..catch..then 的結構就相似於ES3中的 try...catch..finally;

再看下面的示例,

Promise.reject(1).then(function(v) {
    console.log('success:' + v);
    return v
}).catch(function(v) {
    console.log('error:' + v);
        return v
}).then(function(v) {
    console.log(v);
    return v;
}).catch(function(v) {
    console.log('error2:'+v);
    return v;
}).then(function(v){
    console.log(v)
});

/*
 * error:1
 * 1
 * 1
 * /

從這個實例中咱們就能夠得知多個catch只會有一個會被觸發,而且是最先的那個。

Promise.all

Promise.all 能夠執行一個由衆多 promise對象組成的數組,並返回一個新的 promise對象,新返回的 promise對象其狀態會根據所執行的 promise對象數組的狀態而定,若是數組中的全部promise對象都是resolved狀態,Promise.all返回的 promise對象纔會觸發 resolved狀態,不然中止 Promise.all 的執行,並返回一個rejected 狀態的promsie對象。

var p1 = Promise.resolve(1);
var p2 = Promise.resolve(2);
var p3 = Promise.resolve(3);

Promise.all([
    p1,
    p2,
    p3
]).then(function(vs){
    console.log(vs)
})

Promise.all 這種以執行最慢的那個異步爲準的特性,可使用它來作網頁資源的預加載。

Promise.race

Promise.all 相同,race也能夠執行衆多promise對象組成的數組,只是不一樣的是,只要在這個數組有一個Promise狀態發生了改變,(resolved或者是rejected)就會使race返回一個新的 promise對象,並且這個對象的狀態也是基於 promise數組執行時的狀態。

var p1 = Promise.resolve(1);
var p2 = Promise.resolve(2);
var p3 = Promise.resolve(3);

Promise.race([
    p1,
    p2,
    p3
]).then(function(vs){
    console.log(vs)
})

若是說 Promise.all 會以Promise數組中最慢的爲準,那麼 race 則會以數組中最早執行的那個Promise對象爲基準,所以利用這一個特性,能夠用 race來設置超時時間。

var p1 = new Promise(function(resolve, reject) {
    setTimeout(function() {
        reject('超時')
    }, 5000)
    setTimeout(function() {
        resolve('success')
    }, 10000)
});
var p2 = new Promise(function(resolve, reject) {

    setTimeout(function() {
        resolve('success')
    }, 6000)
});
var p3 = new Promise(function(resolve, reject) {

    setTimeout(function() {
        resolve('success')
    }, 6000)
});

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

Promise的同步調用

Promise 自己是一個異步對象,當異步對象狀態更改時觸發對應狀態的handle方法。所以若是想讓多個promise對象同步執行,必須將具備依賴關係的 promise對象放置到對應的另外一個promise對象的回調中。

function getAsync(v){

    return new Promise(function(resolve,reject){
        resolve(v);
    });
}

function main(){

    return getAsync(1000).then(pushValue).then(function(){return getAsync(500).then(pushValue)}).then(function(){return getAsync(1500).then(pushValue)})
}

main().then(function(){
    console.log('所有執行完成!');
})

Promise && callback

Promise的本質是維護狀態,偵測狀態,根據狀態進行響應。而callback 則是將自身做爲參數傳入到另外一個方法中,做爲別的方法體的一部分去調用,所以 Promsie要比 Callback靈活的不少。
若是用代碼來作對比的話,promise是這樣的:

Promise.resove(2).then(function(v){return v}).then(function(v){return v})

而 callback的方式則是這樣的:

doAsync1(function () {
  doAsync2(function () {
    doAsync3(function () {
      doAsync4(function () {
    })
  })
})

總的來講,Promsie的優點更體現與鏈式調用,而callback(相比較Promise的劣勢)就體如今嵌套使用

$.Deferred

對於 Promsie 的實現,Jquery有着本身的一套實現方式,那就是JQ的 $.Deferred 方法。
經過調用 $.Deferred 咱們能夠得到一個JQ版的異步對象實例。

簡單實例:

var def = $.Deferred(); //得到一個JQ的異步對象實例。
def.resolve('success').then(function(v){console.log(v)}); // success

是否是與ES6的 promise 徹底同樣?若是你真的這樣認爲那就錯了,繼續看下面的示例:

var def = $.Deferred();
def.then(function(v){console.log(v)});
def.resolve('success');

是否是發現了一個很大的不一樣之處,異步對像實例 def 居然能夠經過resolve() 方法本身修改本身的狀態!而ES6中的Promsie標準規定的是異步對象的狀態不能手動改變,雖然二者不一樣,可是也無需大驚小怪,畢竟JQ的 $.Deferred 有着本身的實現標準。並且,JQ也提供了另外一種受限的異步對象,這個受限的異步對象,基本上就與ES6的 Promise基本一致了。

可是這個受限的異步對象必需要配合必定的寫法,才能避免被手動更改狀態。

function getAsync() {
    var def = $.Deferred();
    setTimeout(function() {
        def.resolve('success');
    }, 1000);
    return def.promise(); //返回一個受限制的異步對象實例。
}

var dep = getAsync();

dep.then(function(v) {
    console.log(v);
    return v
}).then(function(v) {
    console.log(v);
    return v
});

咱們能夠經過比較受限與不受限的兩種異步對象實例,從而更直觀的瞭解這這二者的區別:

console.log($.Deferred());
console.log($.Deferred().promise());

經過打印這兩種異步對象,咱們明顯能夠看到受限的對象其含有的方法要遠遠少於沒有受限的實例對象,而其中最明顯的就是受限的實例對象並不具備 resolve 方法,這也就直接的說明了受限的異步對象是沒法直接修改本身的狀態。

因爲使用最多的仍是受限的異步對象,因此這裏咱們就大體的說下受限的異步對象具備的一些方法。

then

JQ中異步對象實例的 then 方法與ES6的Promise 對象實例的 then 方法使用格式與功能基本相同,惟一不一樣的就是JQ對 then 方法的回調處理進行了擴展,加入了 pedding 狀態時的回調。

function getAsync() {
    var def = $.Deferred();
    def.notify('loading'); //指定pedding時觸發回調,並傳入參數。
    setTimeout(function() {
        def.resolve('success');
    }, 1000);
    return def.promise(); //返回一個受限制的異步對象實例。
}

var dep = getAsync();

dep.then(function(v) {
    console.log(v)
}, function() {}, function(v) {
    console.log(v)
});

done/fail/progress

done()fail()progress() 等方法都是對 then() 方法的功能包裝。
done() 表示resolved狀態時的處理方法,fail() 表示 rejected 狀態時的處理方法,progress() 表示pendding 狀態時的處理方法。

function getAsync() {
    var def = $.Deferred();
    def.notify('loading')
    setTimeout(function() {
        def.reject('fail');
    }, 1000);
    return def.promise(); //返回一個受限制的異步對象實例。
}

var dep = getAsync();

dep.done(function(v){
    console.log(v);
});
dep.fail(function(v){
    console.log(v);
});
dep.progress(function(v){
    console.log(v);
})

always
經過JQ Deferred().promise() 方法返回的受限的異步對象實例中,always 方法相似於 try..catch..finally 中的 finally,不論異步對象的狀態是成功仍是失敗,都會觸發該方法。

function getAsync() {
    var def = $.Deferred();
    setTimeout(function() {
        def.resolve('success');
    }, 1000);
    return def.promise(); //返回一個受限制的異步對象實例。
}

var dep = getAsync();

dep.always(function(v){console.log(v)});

state

經過調用 state() 方法能夠得到當前對象實例的狀態。

$.Deferred().promise().state(); //pending

初探Promise的基本實現

學習一門技術或者是一個工具,最好的辦法,莫非於瞭解它們的大體實現,這裏我以本身的方式,編寫一個簡單的 Promise 對象。
如今只是一個簡單的示例,功能還很是簡陋只有 then、resolve、reject等功能,並且還有不少bug,可是也足夠讓我對 promise 有更進一步的認識。

function Deferred(fn) {

    var _this = this;

    var doneList = []; // 用於保存 then方法中 resolved狀態時的回調函數.
    var failCallbck = []; // 用於保存 then方法中 rejected狀態時的回調函數.

    this.PromiseValue = undefined; //promsie的值
    this.PromiseStatus = 'pending'; //promise的狀態

    function resolve(v) { //resolve狀態的執行函數

        setTimeout(function() {    //脫離同步代碼,以異步的方式執行 then 方法中的代碼。
            _this.PromiseStatus = 'resolved';
            _this.PromiseValue = v;
            doneList.forEach(function(self, index) {
                _this.PromiseValue = self(_this.PromiseValue); //保存then方法中回調的return值,以供鏈式調用時下個then回調函數使用。
            });
        }, 0);

    }

    function reject(v) { //reject狀態的執行函數

        var idx; //定位failCallbck中最後的失敗處理函數的索引。 eg: [1,...,n] 1表示最先最後,n表示最早最近的
        setTimeout(function() { //脫離同步代碼,以異步的方式執行 then 方法中的代碼。

            _this.PromiseStatus = 'rejected';
            _this.PromiseValue = v;
            failCallbck.forEach(function(f, i) {
                if (typeof f != 'undefined' && typeof f == 'function') {
                    idx = i;
                    _this.PromiseValue = f(_this.PromiseValue);
                    return; //對於異常處理函數,只會執行最後的那一個。
                }
            });

            _this.PromiseStatus = 'resolved';

            //遍歷執行doneList中 resolved 狀態時的回調函數,可是忽略rejected處理函數以前的全部resolve 狀態的回調函數
            for (var i = idx + 1; i < doneList.length; i++) {
                _this.PromiseValue = doneList[i](_this.PromiseValue);
            }

        }, 0)

    }

    this.then = function(done, faill) {

        if (this.PromiseStatus === 'pending') {
            doneList.push(done);
            failCallbck.push(faill);
        } else if (this.PromiseStatus === 'resolved') {
            done();
        } else {
            failCallbck = faill;
        }

        return this;

    }

    try {
        fn(resolve, reject);
    } catch (e) {
        throw new Error(e);
    }
}

調用:

var def1 = new Deferred(function(resolve, reject) {
    resolve(1);
});
var def2 = new Deferred(function(resolve, reject) {
    reject(2);
});

def1.then(function(e) {
    console.log('success:' + e);
    return e + 1;
}, function(e) {
    console.log('error:' + e);
    return e + 1;
}).then(function(v) {
    console.log(v);
});

def2.then(function(e) {
    console.log('success:' + e);
    return e + 1;
}, function(e) {
    console.log('error:' + e);
    return e + 1;
}).then(function(v) {
    console.log(v);
});

Promise的常見問題

Promise的同步與異步執行

var p = new Promise(function(resolve,reject){
    console.log(1);
    resolve(2);
    console.log(4)
});

p.then(function(){
    console.log(3);
})
console.log(5)

在實例化對象的時候,代碼的執行依然是同步執行,而實例化對象的狀態回調函數 then,catch 纔是異步執行。

狀態改變

var p = new Promise(function(resolve,reject){
    resolve('success1');
    reject('error1');
    resolve('success2');
});

p.then(function(e){console.log(e)}).catch(function(v){console.log(v)})

Promise 的狀態一旦肯定將沒法改變。

異常狀況下的Promise

Promise.resolve(1).then(function(v) {
    console.log(v);
    return new Error('error!!!');
}).then(function(v) {
    console.log(v);
}).catch(function(e) {
    console.log(e);
});

resolve() 進入了第一個then的回調,雖然返回了一個 error 類型,可是是經過 return 返回的,因此它將會被做爲第二個 then 的值接收,並不會改變promsie的狀態,因此後面的 catch 便不會被觸發。
若是想觸發catch也很簡單,只須要使用下面兩種方式的任何一種便可。

  • return new Promise().reject(1)
  • throw new Error('xxx')
Promise.resolve(1).then(function(v) {
    console.log(v);
    return new Error('error!!!');
}).then(function(v) {
    return Promise.reject(1);
}).catch(function(e) {
    console.log(e);
});

雖然觸發了最後的 catch 回調,可是否與 Promise 定義的標準相悖呢?畢竟 promsie對象的狀態一經發生,便沒法改變的...實際上並非如此,由於咱們以前已經說過, then、catch 等都會返回要給新的 promise 對象,並且這裏 return Promise.reject(1) 返回的自己就是一個新的對象。

值的穿透

Promise.resolve(1)
  .then(2)
  .then(Promise.resolve(3))
  .then(console.log)

.then 或者 .catch 的參數指望是函數,傳入非函數則會發生值穿透。


參考
http://liubin.org/promises-book/ (開源Promise 迷你書)
https://zhuanlan.zhihu.com/p/30797777 Promise 必知必會(十道題)

相關文章
相關標籤/搜索