本文是基於對阮一峯的Promise文章的學習整理筆記,整理了文章的順序、增長了更多的例子,使其更好理解。javascript
在Promise以前,在js中的異步編程都是採用回調函數和事件的方式,可是這種編程方式在處理複雜業務的狀況下,很容易出現callback hell(回調地獄)
,使得代碼很難被理解和維護。java
Promise就是改善這種情形的異步編程的解決方案,它由社區最先提出和實現,es6將其寫進了語言標準,統一了用法,而且提供了一個原生的對象Promise
。node
咱們經過一個簡單例子先來感覺一下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()編程
咱們在經過Promise構造函數實例化一個對象時,會傳遞一個函數做爲參數,那麼這個函數有什麼特色?數組
答案就是在新建一個Promise後,這個函數會當即執行。promise
let promise = new Promise(function (reslove, reject) { console.log('Promise'); }); console.log('end');
執行結果以下:
能夠看到是先輸出了Promise
,再輸出了end
。
在Promise中,對一個異步操做作出了抽象的定義,Promise操做只會處在3種狀態的一種,他們之間的轉化如圖所示
注意,這種狀態的改變只會出現從未完成態向完成態或失敗態轉化,不能逆反。完成態和失敗態不能互相轉化,並且,狀態一旦轉化,將不能更改。
只有異步操做的結果能夠決定當前是哪種狀態,任何其餘操做都沒法改變這個狀態。這也是Promise這個名字的由來,它的英語意思是承諾,表示其餘手段沒法改變。
在聲明一個Promise對象實例時,咱們傳入的匿名函數參數中:
resolve
就對應着完成態以後的操做
reject
對應着失敗態以後的操做
那麼問題來了,then()方法有什麼做用?resolve和reject又是從哪裏傳遞過來的?
其實這兩個問題是一個問題,在實例化一個Promise對象以後,咱們調用該對象實例的then()
方法傳遞的兩個參數中:
第一個參數(函數)對應着完成態的操做,也就是resolve
第二個參數(函數)對應着失敗態的操做,也就是reject
那就是說,在Promise中是經過then()方法來指定處理異步操做結果的方法。
到這裏咱們明白了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,執行的效果爲:
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
。
從上面的例子,咱們能夠了解到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環境下執行,執行結果爲:
catch()方法是Promise實例的方法,即Promise.prototype
上的屬性,它實際上是.then(null, rejection)
的簡寫,用於指定發生錯誤時的回調。
這個方法其實很簡單,在這裏並不想討論它的使用,而是想討論的是Promise中的錯誤的捕抓和處理。
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方法
跟傳統的
try/catch
不一樣的是,若是沒有使用catch
方法指定錯誤處理回調函數,則Promise對象拋出的錯誤不會傳遞到外層代碼(在chrome會報錯)
Node.js有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤。如下代碼就是在node環境下運行。
var p = new Promise((resolve, reject) => { resolve(x + 2); }); p.then( () => { console.log('nothing'); });
沒錯,既然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); });
Promise.all()方法用於將多個Promise實例,包裝成一個新的Promise實例,例如
var p = Promise.all([p1, p2, p3]);
新的Promise實例p
的狀態由p1, p2, p3
決定:
當p1, p2, p3
的狀態都爲完成態
時,p爲完成態。
p1, p2, p3
中任一一個狀態爲失敗態
,則p爲失敗態。
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()));
Promise.resolve()能夠將現有的對象轉爲Promise對象。
var p = Promise.resolve('p'); // 至關於 var p = new Promise(resolve => resolve('p'));
比較有意思的是Promise.resolve()會根據參數類型進行相應的處理,分幾種狀況討論。
參數是一個Promise實例,那麼Promise.resolve將不作任何處理,直接返回這個實例。
參數是一個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
。
若是參數是一個原始值,或者不具備then方法的對象,則Promise.resolve方法返回一個新的Promise對象,狀態爲resolve
,而後直接將該參數傳遞給resolve
方法。
var p = Promise.resolve("p"); p.then( ret => console.log(ret)); // p
Promise.resolve方法不帶參數時,會直接返回一個resolve
狀態的Promise對象。
須要注意的當即resolve
的Promise對象,是在本輪事件循環
的結束時,而不是下一輪事件循環
的開始執行。示例代碼:
setTimeout(() => console.log('3'), 0); var p = Promise.resolve(); p.then(() => console.log('2')); console.log('1');
輸出結果爲:
Promise.reject()返回一個新的Promise實例,該實例的狀態爲rejected
,對於傳入的參數的處理跟Promise.resolve相似,就是狀態都爲rejected
。
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中的回調函數是在全局做用域中執行的
,所以拋出的錯誤就是在全局做用域上。
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 }) ); };
從上面幾個小節綜合來看,能夠看到Promise其實就是作了一件事情,那就是對異步操做進行了封裝,而後能夠將異步操做以同步的流程表達出來,避免了層層嵌套的回調函數,同時提供統一的接口,使得控制異步操做更加容易。
可是,Promise也有一些缺點:
沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。
若是不設置回調函數,Promise內部的錯誤不會反應到外部。
當處於未完成態時,沒法得知目前進展到哪個階段。
使用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方法。