promise的原理


promise簡介

Promise的出現,原本是為了解決回調地獄的問題。所有人在講解Promise時,都會以一個ajax請求為例,此處我們也用一個簡單的ajax的例子來帶大家看一下Promise是如何使用的。

ajax請求的傳統寫法:

getData(method, url, successFun, failFun){ var xmlHttp = new XMLHttpRequest(); xmlHttp.open(method, url); xmlHttp.send(); xmlHttp.onload = function () { if (this.status == 200 ) { successFun(this.response); } else { failFun(this.statusText); } }; xmlHttp.onerror = function () { failFun(this.statusText); }; }

改為promise后的寫法:

getData(method, url){ var promise = new Promise(function(resolve, reject){ var xmlHttp = new XMLHttpRequest(); xmlHttp.open(method, url); xmlHttp.send(); xmlHttp.onload = function () { if (this.status == 200 ) { resolve(this.response); } else { reject(this.statusText); } }; xmlHttp.onerror = function () { reject(this.statusText); }; }) return promise; } getData('get','www.xxx.com').then(successFun, failFun)

很顯然,我們把異步中使用回調函數的場景改為了.then().catch()等函數鏈式調用的方式。基於promise我們可以把復雜的異步回調處理方式進行模塊化。

下面,我們就來介紹一下Promise到底是個什么東西?它是如何做到的?

Promise的原理分析

其實promise原理說起來並不難,它內部有三個狀態,分別是pendingfulfilledrejected 

pending是對象創建后的初始狀態,當對象fulfill(成功)時變為fulfilled,當對象reject(失敗)時變為rejected。且只能從pengding變為fulfilledrejected ,而不能逆向或從fulfilled變為rejected 、從rejected變為fulfilled。如圖所示:

Promise實例方法介紹

Promise對象擁有兩個實例方法then()catch()

從前面的例子中可以看到,成功和失敗的回調函數我們是通過then()添加,在promise狀態改變時分別調用。promise構造函數中通常都是異步的,所以then方法往往都先於resolvereject方法執行。所以promise內部需要有一個存儲fulfill時調用函數的數組和一個存儲reject時調用函數的數組。

從上面的例子中我們還可以看到then方法可以接收兩個參數,且通常都是函數(非函數時如何處理下一篇文章中會詳細介紹)。第一個參數會添加到fulfill時調用的數組中,第二個參數添加到reject時調用的數組中。當promise狀態fulfill時,會把resolve(value)中的value值傳給調用的函數中,同理,當promise狀態reject時,會把reject(reason)中的reason值傳給調用的函數。例:

var p = new Promise(function(resolve, reject){ resolve(5) }).then(function(value){ console.log(value) //5 }) var p1 = new Promise(function(resolve, reject){ reject(new Error('錯誤')) }).then(function(value){ console.log(value) }, function(reason){ console.log(reason) //Error: 錯誤(…) })

then方法會返回一個新的promise,下面的例子中p == p1將返回false,說明p1是一個全新的對象。

var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(function(value){ console.log(value) }) p == p1 // false 

這也是為什么then是可以鏈式調用的,它是在新的對象上添加成功或失敗的回調,這與jQuery中的鏈式調用不同。

那么新對象的狀態是基於什么改變的呢?是不是說如果p的狀態fulfill,后面的then創建的新對象都會成功;或者說如果p的狀態reject,后面的then創建的新對象都會失敗?

var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(function(value){ console.log(value) // 5 }).then(function(value){ console.log('fulfill ' + value) // fulfill undefined }, function(reason){ console.log('reject ' + reason) })

上面的例子會打印出5和"fulfill undefined"說明它的狀態變為成功。那如果我們在p1then方法中拋出異常呢?

var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(function(value){ console.log(value) // 5 throw new Error('test') }).then(function(value){ console.log('fulfill ' + value) }, function(reason){ console.log('reject ' + reason) // reject Error: test })

理所當然,新對象肯定會失敗。

反過來如果p失敗了,會是什么樣的呢?

var p = new Promise(function(resolve, reject){ reject(5) }) var p1 = p.then(undefined, function(value){ console.log(value) // 5 }).then(function(value){ console.log('fulfill ' + value) // fulfill undefined }, function(reason){ console.log('reject ' + reason) })

說明新對象狀態不會受到前一個對象狀態的影響。

再來看如下代碼:

var p = new Promise(function(resolve, reject){ reject(5) }) var p1 = p.then(function(value){ console.log(value) }) var p2 = p1.then(function(value){ console.log('fulfill ' + value) }, function(reason){ console.log('reject ' + reason) // reject 5 })

我們發現p1的狀態變為rejected,從而觸發了then方法第二個參數的函數。這似乎與我們之前提到的有差異啊,p1的狀態受到了p的狀態的影響。

再來看一個例子:

var p = new Promise(function(resolve, reject){ resolve(5) }) var p1 = p.then(undefined, function(value){ console.log(value) }) var p2 = p1.then(function(value){ console.log('fulfill ' + value) // fulfill 5 }, function(reason){ console.log('reject ' + reason) })

細心的人可能會發現,該例子中then第一個參數是undefined,且value值5被傳到了p1成功時的回調函數中。上面那個例子中then的第二個參數是undefined,同樣reason值也傳到了p1失敗時的回調函數中。這是因當對應的參數不為函數時,會將前一promise的狀態和值傳遞下去。

promise含有一個實例方法catch,從名字上我們就看得出來,它和異常有千絲萬縷的關系。其實catch(onReject)方法等價於then(undefined, onReject),也就是說如下兩種情況是等效的。

new Promise(function(resolve, reject){ reject(new Error('error')) }).then(undefined, function(reason){ console.log(reason) // Error: error(…) }) new Promise(function(resolve, reject){ reject(new Error('error')) }).catch(function(reason){ console.log(reason) // Error: error(…) })

我們提到參數不為函數時會把值和狀態傳遞下去。所以我們可以在多個then之后添加一個catch方法,這樣前面只要reject或拋出異常,都會被最后的catch方法處理。

new Promise(function(resolve, reject){ resolve(5) }).then(function(value){ taskA() }).then(function(value){ taskB() }).then(function(value){ taskC() }).catch(function(reason){ console.log(reason) })

Promise的靜態方法

Promise還有四個靜態方法,分別是resolverejectallrace,下面我們一一介紹。

除了通過new Promise()的方式,我們還有兩種創建Promise對象的方法:

Promise.resolve() 它相當於創建了一個立即resolve的對象。如下兩段代碼作用相同:

Promise.resolve(5) new Promise(function(resolve){ resolve(5) })

它使得promise對象直接resolve,並把5傳到后面then添加的成功函數中。

Promise.resolve(5).then(function(value){ console.log(value) // 5 })

Promise.reject() 很明顯它相當於創建了一個立即reject的對象。如下兩段代碼作用相同:

Promise.reject(new Error('error')) new Promise(function(resolve, reject){ reject(new Error('error')) })

它使得promise對象直接reject,並把error傳到后面catch添加的函數中。

Promise.reject(new Error('error')).catch(function(reason){ console.log(reason) // Error: error(…) })

Promise.all() 它接收一個promise對象組成的數組作為參數,並返回一個新的promise對象。

當數組中所有的對象都resolve時,新對象狀態變為fulfilled,所有對象的resolvevalue依次添加組成一個新的數組,並以新的數組作為新對象resolvevalue,例:

Promise.all([Promise.resolve(5), Promise.resolve(6), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) // fulfill [5, 6, 7] }, function(reason){ console.log('reject',reason) })

當數組中有一個對象reject時,新對象狀態變為rejected,並以當前對象rejectreason作為新對象rejectreason

Promise.all([Promise.resolve(5), Promise.reject(new Error('error')), Promise.resolve(7), Promise.reject(new Error('other error')) ]).then(function(value){ console.log('fulfill', value) }, function(reason){ console.log('reject', reason) // reject Error: error(…) }) 

那當數組中,傳入了非promise對象會如何呢?

Promise.all([Promise.resolve(5), 6, true, 'test', undefined, null, {a:1}, function(){}, Promise.resolve(7) ]).then(function(value){ console.log('fulfill', value) // fulfill [5, 6, true, "test", undefined, null, Object, function, 7] }, function(reason){ console.log('reject', reason) }) 

我們發現,當傳入的值為數字、boolean、字符串、undefined、null、{a:1}、function(){}等非promise對象時,會依次把它們添加到新對象resolve時傳遞的數組中。

那數組中的多個對象是同時調用,還是一個接一個的依次調用呢?我們再看個例子

function timeout(time) { return new Promise(function (resolve) { setTimeout(function () { resolve(time); }, time); }); } console.time('promise') Promise.all([ timeout(10), timeout(60), timeout(100) ]).then(function (values) { console.log(values); [10, 60, 100] console.timeEnd('promise'); // 107ms });

由此我們可以看出,傳入的多個對象幾乎是同時執行的,因為總的時間略大於用時最長的一個對象resolve的時間。

Promise.race() 它同樣接收一個promise對象組成的數組作為參數,並返回一個新的promise對象。

Promise.all()不同,它是在數組中有一個對象(最早改變狀態)resolvereject時,就改變自身的狀態,並執行響應的回調。

Promise.race([Promise.resolve(5), Promise.reject(new Error('error')), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) // fulfill 5 }, function(reason){ console.log('reject',reason) }) Promise.race([Promise.reject(new Error('error')), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) }, function(reason){ console.log('reject',reason) //reject Error: error(…) }) 

且當數組中有非異步Promise對象或有數字、boolean、字符串、undefined、null、{a:1}、function(){}等非Promise對象時,都會直接以該值resolve

Promise.race([new Promise((resolve)=>{ setTimeout(()=>{ resolve(1) },100)}), Promise.resolve(5), "test", Promise.reject(new Error('error')), Promise.resolve(7)]).then(function(value){ console.log('fulfill', value) // fulfill 5 }, function(reason){ console.log('reject',reason) }) // fulfill 5

數組中第一個元素是異步的Promise,第二個是非異步Promise,會立即改變狀態,所以新對象會立即改變狀態並把5傳遞給成功時的回調函數。

那么問題又來了,既然數組中第一個元素成功或失敗就會改變新對象的狀態,那數組中后面的對象是否會執行呢?

function timeout(time) { return new Promise(function (resolve) { setTimeout(function () { console.log(time) resolve(time); }, time); }); } console.time('promise') Promise.race([ timeout(10), timeout(60), timeout(100) ]).then(function (values) { console.log(values); [10, 60, 100] console.timeEnd('promise'); // 107ms }); // 結果依次為 // 10 // 10 // promise: 11.1ms // 60 // 100

說明即使新對象的狀態改變,數組中后面的promise對象還會執行完畢,其實Promise.all()中即使前面reject了,所有的對象也都會執行完畢。規范中,promise對象執行是不可以中斷的。

補充

promise對象即使立馬改變狀態,它也是異步執行的。如下所示:

Promise.resolve(5).then(function(value){ console.log('后打出來', value) }); console.log('先打出來')  // 結果依次為 // 先打出來 // 后打出來 5

但還有一個有意思的例子,如下:

setTimeout(function(){console.log(4)},0); new Promise(function(resolve){ console.log(1) for( var i=0 ; i<10000 ; i++ ){ i==9999 && resolve() } console.log(2) }).then(function(){ console.log(5) }); console.log(3); 

結果是 1 2 3 5 4,命名4是先添加到異步隊列中的,為什么結果不是1 2 3 4 5呢?這個涉及到Event loop,后面我會單獨講一下。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM