ES6學習筆記(十二)異步解決方案Promise

1.Promise 的含義

Promise 是異步編程的一種解決方案,比傳統的解決方案——回調函數和事件——更合理和更強大。它由社區最先提出和實現,ES6 將其寫進了語言標準,統一了用法,原生提供了Promise對象html

所謂Promise,簡單說就是一個容器,裏面保存着某個將來纔會結束的事件(一般是一個異步操做)的結果node

從語法上說,Promise 是一個對象,從它能夠獲取異步操做的消息。Promise 提供統一的 API,各類異步操做均可以用一樣的方法進行處理。編程

特色:json

(1)對象的狀態不受外界影響Promise對象表明一個異步操做,有三種狀態:pending(進行中)、resolved(已成功)和 rejected(已失敗)。Promise的英語意思就是「承諾」,表示其餘手段沒法改變。api

(2)一旦狀態改變,就不會再變,任什麼時候候均可以獲得這個結果。Promise對象的狀態改變,只有兩種可能:從pending變爲resolved和從pending變爲rejected數組

有了Promise對象,就能夠將異步操做以同步操做的流程表達出來避免了層層嵌套的回調函數。此外,Promise對象提供統一的接口,使得控制異步操做更加容易。promise

Promise也有一些缺點。瀏覽器

  • 首先,沒法取消Promise,一旦新建它就會當即執行,沒法中途取消。
  • 其次,若是不設置回調函數,Promise內部拋出的錯誤,不會反應到外部。
  • 第三,當處於pending狀態時,沒法得知目前進展到哪個階段(剛剛開始仍是即將完成)。

若是某些事件不斷地反覆發生,通常來講,使用 Stream 模式是比部署Promise更好的選擇。服務器

2. Promise基本用法

const promise = new Promise(function (resolve, reject) { 
    //some code ...
    if (success) {
        resolve(value);
    }else{
        reject(error);
    }
});

ES6 規定,Promise對象是一個構造函數,用來生成Promise實例。異步

Promise實例接收一個函數做爲參數,該函數固定兩個參數分別是resolve(經過)和reject (拒絕),分別用來表示異步回調的執行結果,它們是兩個函數,由 JavaScript 引擎提供,不用本身部署。

上面這種以函數表達式的方式建立的promise實例的時候,因爲調用了構造函數,因此建立實例的同時參數函數function會被執行,而把promise實例做爲返回值放在函數裏面的時候就不會當即執行。

resolve和reject函數

resolve函數的做用是,將Promise對象的狀態從「未完成」變爲「成功」(即從 pending 變爲 resolved),在異步操做成功時調用,並將異步操做的結果,做爲參數傳遞出去(resolve(success));

reject函數的做用是,將Promise對象的狀態從「未完成」變爲「失敗」(即從 pending 變爲 rejected),在異步操做失敗時調用,並將異步操做報出的錯誤,做爲參數傳遞出去(reject(error))。

Promise實例生成之後,能夠用then方法分別指定resolved狀態和rejected狀態的回調函數。

通常的Promise使用方式,包裝一個異步操做。

1 function timeout(ms) {
2     return new Promise((resolve, reject) => {
3         setTimeout(resolve, ms, 'done');
4     });
5 }
6 
7 timeout(100).then((value) => {
8     console.log(value);//done
9 })

上面這種以promise做爲返回對象的只會在調用的時候執行,而後用函數返回的promise實例再調用then方法拿到異步執行的結果。

下面是異步加載圖片的例子。

 1 function loadImageAsync(url) {
 2     return new Promise((resolve, reject) => {
 3         const img = new Image();//圖片構造函數
 4     
 5         img.onload = function () { resolve(img) }//加載完成調用resolve函數返回
 6     
 7         img.onerror = function () { reject(new Error('Could not load image at ' + url)) }//加載出錯調用reject函數返回
 8     
 9         img.src = url;
10     });
11 }

 上面代碼中,使用Promise包裝了一個圖片加載的異步操做。若是加載成功,就調用resolve方法,不然就調用reject方法。

這個被包裝的方法能夠看成同步方法同樣使用,只須要接收其成功或失敗的返回值,而無需使用回調函數獲取結果。

若是調用resolve函數和reject函數時帶有參數,那麼它們的參數會被傳遞給回調函數。reject函數的參數一般是Error對象的實例,表示拋出的錯誤;resolve函數的參數除了正常的值之外,還多是另外一個 Promise 實例,好比像下面這樣。

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

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

上面代碼中,p1p2都是 Promise 的實例,可是p2resolve方法將p1做爲參數,即一個異步操做的結果是返回另外一個異步操做。

注意,這時p1的狀態就會傳遞給p2,也就是說,p1的狀態決定了p2的狀態。若是p1的狀態是pending,那麼p2的回調函數就會等待p1的狀態改變;若是p1的狀態已是resolved或者rejected,那麼p2的回調函數將會馬上執行。

 1 const p1 = new Promise(function (resolve, reject) {
 2     setTimeout(() => reject(new Error('fail')), 3000);
 3 });
 4 
 5 const p2 = new Promise(function (resolve, reject) {
 6     setTimeout(() => resolve(p1), 1000)
 7 });
 8 
 9 p2.then(result => console.log(result)).catch(error => console.log(error));
10 //Error: fail

上面代碼中,p1是一個 Promise,3 秒以後變爲rejectedp2的狀態在 1 秒以後改變,resolve方法返回的是p1因爲p2返回的是另外一個 Promise,致使p2本身的狀態無效了,由p1的狀態決定p2的狀態。因此,後面的then語句都變成針對後者(p1)。又過了 2 秒,p1變爲rejected,致使觸發catch方法指定的回調函數。

注意調用resolvereject並不會終結 Promise 的參數函數的執行

1 new Promise(function (resolve, reject) {
2     resolve(1);
3     console.log(2);
4 }).then(function (result) {  
5     console.log(result);
6 });
7 //2
8 //1

上面代碼中,調用resolve(1)之後,後面的console.log(2)仍是會執行,而且會首先打印出來。這是由於當即 resolved 的 Promise 是在本輪事件循環的末尾執行,老是晚於本輪循環的同步任務

通常來講,調用resolvereject之後,Promise 的使命就完成了,後繼操做應該放到then方法裏面,而不該該直接寫在resolvereject的後面。因此,最好在它們前面加上return語句,這樣就不會有意外。

1 new Promise(function (resolve, reject) {
2     return resolve(1);
3     console.log(2);
4 }).then(function (result) {  
5     console.log(result);
6 });
7 //1

3.then函數

Promise 實例具備then方法,也就是說,then方法是定義在原型對象Promise.prototype上的。Promise.prototype.then()。它的做用是爲 Promise 實例添加狀態改變時的回調函數通俗點來講就是捕獲回調函數的返回值

前面說過,then方法的第一個參數是resolved狀態的回調函數,第二個參數可選)是rejected狀態的回調函數。

then方法返回的是一個新的Promise實例(注意,不是原來那個Promise實例)。所以能夠採用鏈式寫法,即then方法後面再調用另外一個then方法。

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

上面的代碼使用then方法,依次指定了兩個回調函數。第一個回調函數完成之後,會將返回結果做爲參數,傳入第二個回調函數

採用鏈式的then,能夠指定一組按照次序調用的回調函數。這時,前一個回調函數,有可能返回的仍是一個Promise對象(即有異步操做),這時後一個回調函數,就會等待該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。固然,通常then方法的第二個函數參數省略,而用catch函數統一捕獲error。

若是採用箭頭函數,上面的代碼能夠寫得更簡潔。簡潔也就意味着很差理解。。。

getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

4.catch函數

Promise.prototype.catch方法是.then(null, rejection).then(undefined, rejection)的別名,用於指定發生錯誤時的回調函數通俗點來講就是捕獲回調函數拋出的異常

 1 // 寫法一
 2 const promise = new Promise(function(resolve, reject) {
 3   try {
 4     throw new Error('test');
 5   } catch(e) {
 6     reject(e);
 7   }
 8 });
 9 promise.catch(function(error) {
10   console.log(error);
11 });
12 
13 // 寫法二
14 const promise = new Promise(function(resolve, reject) {
15   reject(new Error('test'));
16 });
17 promise.catch(function(error) {
18   console.log(error);
19 });

reject方法拋出回調函數的異常結果,通常由catch函數捕獲並處理,不然可能會影響進程執行。

resolve方法拋出回調函數的正常結果,由then函數接收處理。

若是 Promise 狀態已經變成resolved,再拋出錯誤是無效的。

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

上面代碼中,Promise 在resolve語句後面,再拋出錯誤,不會被捕獲,等於沒有拋出。由於 Promise 的狀態一旦改變,就永久保持該狀態,不會再變了。

Promise 對象的錯誤具備「冒泡」性質,會一直向後傳遞,直到被捕獲爲止。也就是說,錯誤老是會被下一個catch語句捕獲。因此一連串的then方法連續調用只須要一個catch就能夠捕獲異常了。

then/catch寫法和傳統的try/catch寫法非常類似,then 約等於 try,跟傳統的try/catch代碼塊不一樣的是,若是沒有使用catch方法指定錯誤處理的回調函數,Promise 對象拋出的錯誤不會傳遞到外層代碼,即不會有任何反應。可是會報個unhandledRejection警告。

 1 // bad---不建議這麼寫
 2 promise
 3   .then(function(data) {
 4     // success
 5   }, function(err) {
 6     // error
 7   });
 8 
 9 // good---應該這麼寫
10 promise
11   .then(function(data) { //cb
12     // success
13   })
14   .catch(function(err) {
15     // error
16   });
 1 const someAsyncThing = function() {
 2   return new Promise(function(resolve, reject) {
 3     // 下面一行會報錯,由於x沒有聲明
 4     resolve(x + 2);
 5   });
 6 };
 7 
 8 someAsyncThing().then(function() {
 9   console.log('everything is great');
10 });
11 
12 setTimeout(() => { console.log(123) }, 2000);
13 // Uncaught (in promise) ReferenceError: x is not defined
14 // 123

上面代碼中,someAsyncThing函數產生的 Promise 對象,內部有語法錯誤。瀏覽器運行到這一行,會打印出錯誤提示ReferenceError: x is not defined,可是不會退出進程、終止腳本執行,2 秒以後仍是會輸出123。這就是說,Promise 內部的錯誤不會影響到 Promise 外部的代碼,通俗的說法就是「Promise 會吃掉錯誤」

這個腳本放在服務器執行,退出碼就是0(即表示執行成功)。不過,Node 有一個unhandledRejection事件,專門監聽未捕獲的reject錯誤,上面的腳本會觸發這個事件的監聽函數,能夠在監聽函數裏面拋出錯誤。

注意,Node 有計劃在將來廢除unhandledRejection事件。若是 Promise 內部有未捕獲的錯誤,會直接終止進程,而且進程的退出碼不爲 0,因此Promise的調用必定要捕獲異常。

通常老是建議,Promise 對象後面要跟catch方法,這樣能夠處理 Promise 內部發生的錯誤。catch方法返回的仍是一個 Promise 對象,所以後面還能夠接着調用then方法

Promise.resolve()
.catch(function(error) {
  console.log('oh no', error);
})
.then(function() {
  console.log('carry on');
});
// carry on

也就是說,當連續調用多個回調函數順序執行的時候,能夠一個回調一個catch,也能夠多個回調一個catch,catch和then能夠間隔使用,區別在於若是隻在最後使用一個catch捕獲全部異常,那當中間某個回調拋出異常時,後面的將不會再執行。

 1 new Promise(function (resolve, reject) {
 2     return resolve(111);
 3 }).then(function (result) {  
 4     return new Promise(function (resolve, reject) { resolve(result + 111) });
 5 }).then(function (result) {
 6     return new Promise(function (resolve, reject) { resolve(result + 111) });
 7 }).then(function (result) {
 8     console.log(result);
 9     return new Promise(function (resolve, reject) { reject(new Error('222')) });
10 }).catch(function (error) {
11     console.log(error);
12 });
13 // 333
14 // Error: 222

上面的代碼定義了一串順序執行的回調函數,上一個的返回值做爲下一個的參數,每一個回調都返回一個Promise實例,而後鏈式調用,最後使用catch捕獲異常。

上面then方法裏面執行的是匿名的回調,固然能夠調用定義好的函數

p1().then(p2()).then(p3()).then(p4()).catch(error);

p1, p2, p3, p4都是用Promise包裹的回調函數,使用這種方法讓其順序執行,能夠用箭頭函數來簡化具體的過程。

 1 function p1(result) {
 2     return new Promise((resolve, reject) => {
 3         resolve(result + 111);
 4     });
 5 }
 6 function p2(result) {
 7     return new Promise((resolve, reject) => {
 8         resolve(result + 111);
 9     });
10 }
11 function p3(result) {
12     return new Promise((resolve, reject) => {
13         resolve(result + 111);
14     });
15 }
16 function p4(result) {
17     return new Promise((resolve, reject) => {
18         reject(new Error(result + 111));
19     });
20 }
21 
22 p1(111).then(result => p2(result)).then(result => p3(result)).then(result => p4(result)).catch(error => {console.log(error)});
23 // Error: 555

上面是簡化後的順序執行例子,p1傳入111,最後p4異常結果輸出555,這即是then和catch函數的用法。

 5.finally函數

就像try-catch-finally同樣,也有then-catch-finally結構,finally一樣用來指定那些必須必定會執行大的操做,好比關閉流。該方法是 ES2018 引入標準的。

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

finally方法的回調函數不接受任何參數,這意味着沒有辦法知道,前面的 Promise 狀態究竟是fulfilled仍是rejected

這代表,finally方法裏面的操做,應該是與狀態無關的,不依賴於 Promise 的執行結果

finally本質上是then方法的特例

 1 promise
 2 .finally(() => {
 3   // 語句
 4 });
 5 
 6 // 等同於
 7 promise
 8 .then(
 9   result => {
10     // 語句
11     return result;
12   },
13   error => {
14     // 語句
15     throw error;
16   }
17 );

上面代碼中,若是不使用finally方法,一樣的語句須要爲成功和失敗兩種狀況各寫一次。有了finally方法,則只須要寫一次。

6.Promise.all()

上面使用鏈式調用then方法能夠將多個promise實例順序執行,但Promise的功能固然不只僅於此,對於須要並行的多個異步操做,就須要使用all()方法了。

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

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

Promise.all方法接受一個數組做爲參數p1p2p3都是 Promise 實例,若是不是,就會先調用下面講到的Promise.resolve方法,將參數轉爲 Promise 實例,再進一步處理。而後數組中的promise實例就會並行執行,這是一個公平的並行。(Promise.all方法的參數能夠不是數組,但必須具備 Iterator 接口,且返回的每一個成員都是 Promise 實例。也就是說能夠是set等類數組)。

而後這個新的Promise 實例p的狀態由其子實例 p1, p2, p3的狀態決定,分兩種狀況:

一、成功:子實例所有成功纔算成功(resolve);

二、失敗:子實例只要有一個失敗就算失敗(reject);

有點像邏輯運算符的 &&,全真爲真,一假則假。

注意,若是做爲參數的 Promise 實例,本身定義了catch方法,那麼它一旦被rejected,並不會觸發Promise.all()catch方法。

 1 const p1 = new Promise((resolve, reject) => {
 2   resolve('hello');
 3 })
 4 .then(result => result)
 5 .catch(e => e);
 6 
 7 const p2 = new Promise((resolve, reject) => {
 8   throw new Error('報錯了');
 9 })
10 .then(result => result)
11 .catch(e => e);
12 
13 Promise.all([p1, p2])
14 .then(result => console.log(result))
15 .catch(e => console.log(e));
16 // ["hello", Error: 報錯了]

上面代碼中,p1resolvedp2首先會rejected,可是p2有本身的catch方法,該方法返回的是一個新的 Promise 實例,p2指向的其實是這個實例。該實例執行完catch方法後,也會變成resolved,致使Promise.all()方法參數裏面的兩個實例都會resolved,所以會調用then方法指定的回調函數,而不會調用catch方法指定的回調函數。

若是p2沒有本身的catch方法,就會調用Promise.all()catch方法

7.Promise.race()

然而物競天擇,適者生存,共同富裕只是少數,更多的是你死我活的競爭,race()方法即是用來作這種事。

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

上面代碼中,只要p1p2p3之中有一個實例率先改變狀態,p的狀態就跟着改變。那個率先改變的 Promise 實例的返回值,就傳遞給p的回調函數。

即一組子實例中只要有一個成功,就返回成功結果,其餘的被淘汰,所有失敗,就返回失敗。

就像邏輯運算符的 || : 一真爲真,全假則假。

這種機制能夠定義一個固定時間的回調,用來限制回調的返回時間,而沒必要一直等待結果。

8.Promise.resolve() 

有時須要將現有對象轉爲 Promise 對象Promise.resolve方法就起到這個做用。

Promise.resolve('foo')
// 等價於
new Promise(resolve => resolve('foo'))

Promise.resolve方法的參數分紅四種狀況。

(1)參數是一個 Promise 實例

若是參數是 Promise 實例,那麼Promise.resolve將不作任何修改、原封不動地返回這個實例。

(2)參數是一個thenable對象

 

thenable對象指的是具備then方法的對象,好比下面這個對象。

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

Promise.resolve方法會將這個對象轉爲 Promise 對象,而後就當即執行thenable對象的then方法

 1 let thenable = {
 2   then: function(resolve, reject) {
 3     resolve(42);
 4   }
 5 };
 6 
 7 let p1 = Promise.resolve(thenable);
 8 p1.then(function(value) {
 9   console.log(value);  // 42
10 });

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

(3)參數不是具備then方法的對象,或根本就不是對象

若是參數是一個原始值,或者是一個不具備then方法的對象,則Promise.resolve方法返回一個新的 Promise 對象,狀態爲resolved

const p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

(4)不帶有任何參數

Promise.resolve方法容許調用時不帶參數,直接返回一個resolved狀態的 Promise 對象。

因此,若是但願獲得一個 Promise 對象,比較方便的方法就是直接調用Promise.resolve方法。

const p = Promise.resolve();

p.then(function () {
  // ...
});

須要注意的是,當即resolve的 Promise 對象,是在本輪「事件循環」(event loop)的結束時,而不是在下一輪「事件循環」的開始時,因此打算直接用這種方法定義某個開頭的打算落空了,只能作收尾。

 1 setTimeout(function () {
 2   console.log('three');
 3 }, 0);
 4 
 5 Promise.resolve().then(function () {
 6   console.log('two');
 7 });
 8 
 9 console.log('one');
10 
11 // one
12 // two
13 // three

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

9.Promise.reject()

與resolve()方法相反,Promise.reject(reason)方法也會返回一個新的 Promise 實例,該實例的狀態爲rejected

1 const p = Promise.reject('出錯了');
2 // 等同於
3 const p = new Promise((resolve, reject) => reject('出錯了'))
4 
5 p.then(null, function (s) {
6   console.log(s)
7 });
8 // 出錯了

注意Promise.reject()方法的參數,會原封不動地做爲reject的理由,變成後續方法的參數。這一點與Promise.resolve方法不一致。

 1 const thenable = {
 2   then(resolve, reject) {
 3     reject('出錯了');
 4   }
 5 };
 6 
 7 Promise.reject(thenable)
 8 .catch(e => {
 9   console.log(e === thenable)
10 })
11 // true

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

10.Promise.try()

實際開發中,常常遇到一種狀況:不知道或者不想區分,函數f是同步函數仍是異步操做,可是想用 Promise 來處理它。由於這樣就能夠無論f是否包含異步操做,都用then方法指定下一步流程,用catch方法處理f拋出的錯誤。通常就會採用下面的寫法。

Promise.resolve().then(f)

上面的寫法有一個缺點,就是若是f是同步函數,那麼它會在本輪事件循環的末尾執行。由於promise串行老是按then的順序的。

const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now

上面代碼中,函數f是同步的,可是用 Promise 包裝了之後,就變成異步執行了。

結尾:Promise是優秀的異步解決方案,也是後面async/await方案的本質基礎,化異步爲同步的方式仍是很舒服的。

相關文章
相關標籤/搜索