ES6入門八:Promise異步編程與模擬實現源碼

  • Promise的基本使用入門:
  • ——實例化promise對象與註冊回調
  • ——宏任務與微任務的執行順序
  • ——then方法的鏈式調用與拋出錯誤(throw new Error)
  • ——鏈式調用的返回值與傳值
  • Promise的基本使用進階:
  • ——then、catch、finally的使用
  • ——all、race的使用
  • Promise的實現目的
  • ——鏈式調用解決回調地獄
  • ——異步回調如今與將來任務分離
  • ——信任問題(控制反轉):調用過早、調用過晚(不被調用)、調用次數過少過多、未能傳遞環境和參數、吞掉出現的錯誤和異常
  • Promise的實現原理與模擬實現源碼

 1、Promise的基本使用入門

1.Promise是什麼?html

Promise是用來實現JS異步管理的解決方案,經過實例化Promise對象來管理具體的JS異步任務。前端

從Promise管理回調狀態的角度來看,Promise又一般被稱爲狀態機,在Promise擁有三種狀態:pending、fulfilled、rejected。node

用Promise解決JS異步執行的邏輯來理解,能夠說Promise是一個將來的事件,也就是說Promise管理的任務並非在JS的同步線程上當即執行,而是會等待同步線程的程序執行完之後纔會執行。ajax

 2.建立Promise實例管理一個異步事件,經過then添加(註冊)異步任務:編程

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5         Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10 },(reason) => {
11     console.log("拒絕:" + reason);
12 });

經過示例能夠看到Promise實例化對象須要傳入一個excutor函數做爲參數,這個函數有兩個形參resolve、reject分別表示受理事件與拒絕事件。這兩個事件也就是經過實例對象調用then方法傳入的兩個函數,也就是說then方法傳入的兩個函數分別表示resolve與reject。設計模式

3.微任務與宏任務:數組

在js線程中,XMLHttpRequest網絡請求響應事件、瀏覽器事件、定時器都是宏任務,會被統一放到一個任務隊列中(用task queue1表示)。promise

而由Promise產生的異步任務resolve、reject被稱爲微任務(用task queue2表示)。瀏覽器

這兩種任務的區別就是當異步任務隊列中即有宏任務又有微任務時,不管宏任務比微任務早多久添加到任務隊列中,都是微任務先執行,宏任務後執行。緩存

來看下面這個示例,瞭解Promose微任務與宏任務的執行順序:

 1 setTimeout(function(){
 2     console.log(0); //這是個宏任務,被先添加到異步任務隊列中
 3 },0);
 4 let oP = new Promise((resolve, reject) => {
 5     resolve(1);//這是個微任務,被後添加到異步任務隊列中
 6     console.log(2);//這是第一個同步任務,最早被打印到控制檯
 7 });
 8 oP.then((val) => {
 9     console.log(val);
10 },null);
11 console.log(3);//這也是個同步任務,第二個被打印到控制檯 

測試的打印結果必然是:2 3 1 0;這就是微任務與宏任務的區別,從這一點能夠了解到,JS自身實現的Promise是一個全新的功能並不是語法糖,因此除原生promise之外的promise實現都是一種模擬實現,在模擬實現中基本上都是使用setTimeout來實現Promise異步任務的,因此若是不支持原生Promise瀏覽器使用的是兼容的Promise插件,其Promise異步任務是宏任務,在程序執行時可能會出現與新版本瀏覽器原生的Promise實現的功能會有些差異,這是須要注意的一個小問題。

 4.Promise中的then的鏈式調用與拋出錯誤:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5         Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10 },(reason) => {
11     console.log("拒絕:" + reason);
12 }).then((val) => {
13     console.log("then2 受理:" + val);
14 },(reason) => {
15     console.log("then2 拒絕:" + reason);
16 });

在上面的示例中,第一個then確定出現兩種狀況,受理或者拒絕,這個毫無疑問,可是上面的鏈式調用代碼中第二個then註冊的resolve與reject永遠都只會觸發受理,因此最後的執行結果是:

//第一種狀況:
受理:ok
then2 受理:undefined

//第二種狀況:
拒絕:no
then2 受理:undefined

ES6中的Promise實現的then的鏈式調用與jQuery的Deferred.then的鏈式調用是有區別的,jQuery中實現的鏈式調用的第一個then的受理或者拒絕回調被調用後,後面的then會相應的執行受理或者拒絕。可是ES6中的Promise除第一個then之外後面都是調用受理,這裏不過多的討論jQuery的Deferred的實現,可是這是一個須要注意的問題,畢竟ES6的Promise是總結了前人的經驗的基礎上設計的新功能,在使用與以前的類似的功能時容易出現慣性思惟。

ES6中的Promise.then鏈式調用的正確姿式——拋出錯誤:

這種設計的思考邏輯是:Promise1管理異步任務___>受理                Promise1沒有拋出錯誤:Promise2受理 

                          ___>Promise2管理Promise1___>

                       ___>拒絕                Promise2拋出錯誤:Promise2拒絕

因此前面的示例代碼在拒絕中應該添加拋出錯誤纔是正確的姿式:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 4     },1000);
 5 });
 6 oP.then((val) => {
 7     console.log("受理:" + val);
 8 },(reason) => {
 9     console.log("拒絕:" + reason);
10     throw new Error("錯誤提示...");
11 }).then((val) => {
12     console.log("then2 受理:" + val);
13 },(reason) => {
14     console.log("then2 拒絕:" + reason);
15 });

在第一個then註冊的reject中拋出錯誤,上面的示例的執行結果就會是這樣了:

//第一種狀況:
受理:ok
then2 受理:undefined

//第二種狀況:
拒絕:no
then2 拒絕:Error: 錯誤提示...

好像這樣的結果並不能說明以前的設計思考邏輯,僅僅只能說明then的鏈式調用在reject中拋出錯誤才能觸發後面的reject,可是在咱們的開發中必然會有即使異步正確受理,但不表明受理回調就能正確的執行完,受理的代碼也可能會出現錯誤,因此在第一個then中受理回調也拋出錯誤的話一樣會觸發後面鏈式註冊的reject,看示例:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5         Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10     if(Math.random() * 100 > 30){
11         return "then1受理成功執行完畢!";
12     }else{
13         throw new Error("錯誤提示:then1受理沒有成功執行完成。");
14     }
15 },(reason) => {
16     console.log("拒絕:" + reason);
17     throw new Error("錯誤提示...");
18 }).then((val) => {
19     console.log("then2 受理:" + val);
20 },(reason) => {
21     console.log("then2 拒絕:" + reason);
22 });

這時候整個示例最後的執行結果就會出現三種狀況:

//狀況1:
受理:ok
then2 受理:then1受理成功執行完畢!

//狀況2:
promise.html:24 拒絕:no
then2 拒絕:Error: 錯誤提示...

//狀況3:
受理:ok
then2 拒絕:Error: 錯誤提示:then1受理沒有成功執行完成。

5.then方法鏈式調用的返回值與傳值:

在前面的代碼中,相信你已經發現,若是前面then註冊的回調不返回值或者不拋出錯誤,後面的then接收不到任何值,打印出來的參數爲undefined。這一點也與jQuery中的then有些區別,在jQuery中若是前面的then沒有返回值,後面then註冊的回調函數會繼續使用前面回調函數接收的參數。

在前面的示例中已經有返回值、拋出錯誤和傳值的展現了,這裏重點來看看若是返回值是一個Promise對象,會是什麼結果:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5          Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10     return new Promise((resolve, reject) => {
11         Math.random() * 100 > 60 ? resolve("then1_resolve_ok") : reject("then_resolve_no");
12     });
13     
14 },(reason) => {
15     console.log("拒絕:" + reason);
16     return new Promise((resolve, reject) => {
17         Math.random() * 100 > 60 ? resolve("then1_reject_ok") : reject("then1_reject_no");
18     })
19 }).then((val) => {
20     console.log("then2 受理:" + val);
21 },(reason) => {
22     console.log("then2 拒絕:" + reason);
23 });

以上的示例出現的結果會有四種:

//狀況1、二
受理:ok
then2 受理:then1_resolve_ok / then2 拒絕:then1_resolve_no

//狀況3、四
拒絕:no
then2 受理:then1_reject_ok / then2 拒絕:then1_reject_no

經過示例能夠看到,當前面一個then的回調返回值是一個Promise對象時,後面的then觸發的受理或者拒絕是根據前面返回的Promise對象觸發的受理或者拒絕來決定的。

 2、Promise的基本使用進階

1.在Promise中標準的捕獲異常的方法是catch,雖然前面的示例中使用了reject拒絕的方式捕獲異常,但通常建議使用catch來實現捕獲異常。須要注意的是異常一旦被捕獲就不能再次捕獲,意思就是若是在鏈式調用中前面的reject已經捕獲了異常,後面鏈式調用catch就不能再捕獲。

建議使用catch異常捕獲的代碼結構:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5          Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10 },(reason) => {
11     console.log("拒絕:" + reason);
12     throw new Error("錯誤提示...");
13 }).then((val) => {
14     console.log("then2 受理:" + val);
15 }).catch((err) => {
16     console.log(err);
17 })

 catch不能捕獲的狀況:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5          Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10 },(reason) => {
11     console.log("拒絕:" + reason);
12     throw new Error("錯誤提示...");
13 }).then((val) => {
14     console.log("then2 受理:" + val);
15 }, (reason) => {
16     console.log("then2 拒絕:" + reason);
17 }).catch((err) => {
18     console.log("異常捕獲:",err);
19 })
20 //這種狀況就只能是在第二個then中的reject捕獲異常,catch不能捕獲到異常

一個非技術性的問題,調用了一個空的then會被忽視,後面的then或者catch依然正常執行:

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5          Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10 },(reason) => {
11     console.log("拒絕:" + reason);
12     throw new Error("錯誤提示...");
13 })
14 .then()//這個then會被忽視,若是前面一個then調用了reject拒絕,後面的catch能正常捕獲(或者後面鏈式調用then都能正常執行)
15 .catch((err) => {
16     console.log("異常捕獲:",err);
17 });

2.在Promise方法中,除了finally都會繼續返回Promise對象,並且finally傳入的回調函數必定會被執行,這個跟前面的一種狀況很是相似,就是當前面的then不拋出錯誤的時候,後面的then必定是調用受理,實際上底層的實現也就是同一個邏輯上實現的。只是finally再也不返回Promise對象,但須要注意的是finally註冊的回調函數獲取不到任參數。

 1 let oP = new Promise((resolve, reject) => {
 2     setTimeout(() => {
 3         //使用定時器開啓異步任務,使用隨機數模擬異步任務受理或者拒絕
 4         // (數字大於60表示異步任務成功觸發受理,反之失敗拒絕)
 5          Math.random() * 100 > 60 ? resolve("ok") : reject("no");
 6     },1000);
 7 });
 8 oP.then((val) => {
 9     console.log("受理:" + val);
10 },(reason) => {
11     console.log("拒絕:" + reason);
12     throw new Error("錯誤提示...");
13 }).catch((err) => {
14     console.log("異常捕獲:",err);
15 }).finally(() => {
16     console.log("結束");//這個回調函數接收不到任何參數
17 })

3.Promise給併發處理提供了兩種實現方式all、race,這兩個的處理邏輯很是相似條件運算符的與(&&)或 (||)運算,all就是用來處理當多個Promise所有成功受理就受理自身的受理回調resolve,不然就拒絕reject。race的處理多個Promise只須要一個Promise成功受理就觸發自身的受理回調,不然就拒絕reject。它們處理Promise實例的方式都是將Promise實例對象做爲數組元素,而後將包裹的數組做爲all或race的參數進行處理。

 

這裏使用一段nodejs環境讀取文件代碼來展現Promise.all的使用:

//路徑+文件名:            內容:data
./data/number.txt        "./data/name.txt"
./data/name.txt          "./data/score.tet"
./data/score/txt         "99"

//src目錄結構
--index.js
--data
----number.txt
----name.txt
----score.txt

Promise.all實現文件數據併發讀取:

 1 let fs = require('fs');
 2 
 3 function readFile(path){
 4     return new Promise((resolve,reject) => {
 5         fs.readFile(path,'utf-8', (err,data) => {
 6             if(data){
 7                 resolve(data);
 8             }else{
 9                 reject(err);
10             }
11         });
12     });
13 }
14 
15 Promise.all([readFile("./data/number.txt"),readFile("./data/name.txt"),readFile("./data/score.txt")]).then((val) =>{
16     console.log(val);
17 });

在nodejs環境中執行代碼,打印結果:

node index.js  //執行js文件
[ './data/name.txt', './data/score.txt', '99' ] //打印結果

從示例中能夠看到Promise.all獲取的值是所有Promise實例受理回調傳入的值,而且以數組的方式傳入。

 

接着來看一個Promise.race的示例,這個示例:

 1 var op1 = new Promise((resolve, reject) => {
 2     setTimeout(resolve, 500, "one");
 3 });
 4 var op2 = new Promise((resolve, reject) => {
 5     setTimeout(resolve, 100, "two");
 6 });
 7 Promise.race([op1,op2]).then((val) => {
 8     console.log(val);
 9 });
10 //打印結果:two

Promise.race獲的值是第一個Promise實例受理回調傳入的值。

 

4.Promise.all與Promise.race的傳值規則:

 all:

全部Promise實例受理resolve,即全部異步回調成功的狀況下,將全部Promise實例的resolve接收的參數合併成一個數組,傳遞給Promise.all生成的新的Promise實例的resolve回調處理。

若是有一個失敗的狀況下,即Promise.all生成的新的Promise實例觸發回調reject函數,這個函數會接收到最早失敗的Promise實例經過reject回調傳入的參數。

 

race:

經過Promise.race處理的Promise實例中最早得到結果的Promise實例的參數,傳遞給Promise.race產生的Promise實例,不論成功與失敗,成功就出發resolve函數,失敗就出發reject函數。

 3、Promise的實現目的

1.鏈式調用解決回調地獄:在一開始學習編程的時候咱們必定都寫過一連串的做用域嵌套代碼,來解決一些業務邏輯鏈相對比較長的功能,而後還可能跟同窗炫耀「你看我把這個功能寫出來了,還能正確執行」。不要問我爲何這麼確定,這種事我作過,個人同窗和朋友也有作過。這爲何值得炫耀呢?無非就是面對這種業務邏輯鏈比較長的功能很難保證在那個不環節不出錯,因此能駕馭層層嵌套的代碼的確能夠說很「認真」的在編碼。我在jQuery的ajax的一篇博客中就是用了一個很是詳細的案例展現了回調地獄:jQuery使用(十二):工具方法之ajax的無憂回調(優雅的代碼風格)

這裏我使用第二節中的(3:Promise.all)案例,(應用以前的文件結構)來寫一個文件層級讀取的示例:

 1 //這是一個基於nodejs環境的js示例,請在nodejs環境中執行index.js
 2 let fs = require('fs');
 3 
 4 fs.readFile("./data/number.txt","utf-8",(err,data) => {
 5     if(data){
 6         fs.readFile(data,"utf-8",(err,data) => {
 7             if(data){
 8                 fs.readFile(data,"utf-8",(err,data) => {
 9                     console.log(data);
10                 })
11             }
12         })
13     }
14 });

相信你們遇到這種代碼都會知道這樣的代碼結構,不易於維護,編寫容易出錯而且還不容易追蹤錯誤。下面來看看使用Promise如何迴避這樣的問題,來提升代碼質量:

 1 //這是一個基於nodejs環境的js示例,請在nodejs環境中執行index.js
 2 let fs = require('fs');
 3 
 4 function readFile(path){
 5     return new Promise((resolve,reject) => {
 6         fs.readFile(path,'utf-8', (err,data) => {
 7             if(data){
 8                 resolve(data);
 9             }else{
10                 reject(err);
11             }
12         });
13     });
14 }
15 readFile("./data/number.txt").then((val) => {
16     return readFile(val);//這裏去獲取nama.text的文本數據
17 },(reason) => {
18     console.log(reason);
19 }).then((val) => {
20     return readFile(val);//這裏去獲取score.text的文本數據
21 },(reason) => {
22     console.log(reason);
23 }).then((val) => {
24     console.log(val);//這裏最後打印score.text的文本數據
25 },(reason) => {
26     console.log(reason);
27 });

2.異步回調如今與將來任務分離:

Kyle Simpson大神在《你不知道的js中卷》的第二部分第一章(1.3並行線程)中給我說明了一個我長期混洗的知識點,「異步」與「並行」,他明確的闡述了異步是關於如今和未來的事件間隙,而並不是關於能同時發生的事情。

簡單來講,在js中咱們能夠把同步任務理解爲如今要執行的任務,異步則是未來要執行的任務,我的認爲這是Promise的核心功能,Promise的then本質上就是這樣的設計思路,在實例化的Promise對象的時候就已經調用了回調任務resolve或者reject,可是Promise將這兩個回調任務處理成了異步(微任務)模式,經過前面的應用介紹咱們知道Promise實例化的時候並無添加這兩個任務,而是後面基於同步任務的then添加的,因此resolve和reject才能在將來有真正的任務能夠執行。

利用異步的這種如今與將來的異步設計思路實現了Promise.all和Promise.race,解決了前端回調的競態問題。關於js競態問題能夠了解《你不知道的js中卷》第二部分第一章和第三章的3.1。(這給內容可多可少,可是想一想Kyle Simpson的清晰明瞭的分析思路,建議你們去看他書。)

3.信任問題(控制反轉):

相信你們在應用js開發的時候都使用果相似這樣的代碼:

ajax("...",function(...){ ... })

一般這樣的代碼咱們都會想到插件或者第三方庫,若是這是一個購物訂單,你知道這段代碼存在多大的風險嗎?咱們根本就不知道這個回調函數會被執行多少次,由於怎麼執行是由別讓人的插件和庫來控制的。順着這個思路,在《你不知道的js中卷》的第二部分第二章2.3.1最後,大神提出這樣的追問:調用過早怎麼辦?調用過晚怎麼辦?調用屢次或者次數太少怎麼辦?沒有傳遞參數或者環境怎麼辦?出現錯誤或者異常怎麼辦?這些內容在《你不知道的js中卷》第二部分第三章3.3都詳細的描述了基於Promise的解決方案。

本質上也就是Promise的控制反轉的設計模式,好比前面的ajax()請求能夠這樣來寫:

var oP = new Promise((resolve,reject) => {   
      resolve(...);
});
oP.then((val) => {
    ajax("...",function(...){...});
});

咱們知道,每一個Promise只能決議一次,不管成功或者失敗,因此就不用小心一個購物訂單請求會不會被插件或者第三方庫誤操做發送屢次(這並非絕對的,畢竟ajax回調函數內部怎麼執行仍是別人的代碼,這裏我能只能假設ajax回調函數是可信任的)。

關於Promise的實現目的還有不少,我也只能在這裏列舉一些比較典型的和常見的問題,若是想了解更多我首先建議你們去看我前面屢次提到的書,或者到各大技術論壇瞭解他人的研究和發現,下面接着進入激動人心的Promise源碼部分。

 4、Promise的實現原理與模擬實現源碼

Promise實現標準文檔:https://promisesaplus.com

因爲源碼的複雜性還算比較高,咱們採用分階段實現的方式,從Promise的一部分功能開始而後逐漸完成全部功能。

第一階段:基於Promise的三種狀態:pending、Fulfilled、Rejected實現同步的狀態決議回調任務處理;

第二階段:基於階段一的狀態機實現Promise異步的狀態決議回調任務處理;

第三階段:實現then的鏈式調用;

第四階段:使用setTimeout模擬實現Promise異步回調任務、處理回調任務中的異常、忽略鏈式調用中的空then;

第五階段:實現回調函數返回Promise實例;

第六階段:實現Promise靜態方法race、all;

第七階段:實現Promise原型方法catch、finally、以及擴展一個deferred靜態方法

1.原理分析之狀態:

Promise實例三種狀態:pending、Fulfilled、Rejected,當pending狀態時表示未決議,可轉換成Fulfilled或者Rejected狀態,轉換狀態後不可更改。

Promise實例化時執行excutor函數,並使用try...catch處理excutor可能拋出的錯誤行爲,若是拋出錯誤,將狀態設置爲Rejected(拒絕)。

在原型上定義then方法,實現回調任務處理。

 1 function myPromise(excutor){
 2     var self = this;
 3     self.status = "pending";
 4     self.resolveValue = null; //緩存受理回調的參數
 5     self.rejectReason = null; //緩存拒絕回調的參數
 6     function resolve(value){
 7         if(self.status === "pending"){
 8             self.status = "Fulfilled";
 9             self.resolveValue = value; // 將受理回調執行的參數緩存到Promise實例屬性上
10         }
11     }
12 
13     function reject(reason){
14         if(self.status === "pending"){
15             self.status = "Rejected";
16             self.rejectReason = reason; // 將拒絕回調執行的參數緩存到Promise實例屬性上
17         }
18     }
19     //當excutor拋出錯誤執行reject
20     try{
21         excutor(resolve,reject);
22     }catch(e){
23         reject(e);
24     }
25     
26 };
27 
28 myPromise.prototype.then = function(onFulfilled,onRejected){
29     var self = this;
30     if(self.status === "Fulfilled"){
31         onFulfilled(self.resolveValue);
32     }
33     if(self.status === "Rejected"){
34         onRejected(self.rejectReason);
35     }
36 }

測試代碼:

 1 var myP = new myPromise((resolve,reject) => {
 2     // 測試resolve
 3     // resolve("受理");
 4     // 測試reject
 5     // reject("拒絕");
 6     // 測試拋出錯誤
 7     throw new Error("excutor拋出錯誤");
 8 });
 9 myP.then((val) => {
10     console.log(val);
11 }, (reason) => {
12     console.log(reason);
13 });

2.Promise原理分析之異步:

這部分還不是解析Promise微任務的內容,而是解析當excutor內決議是一個異步任務,好比ajax請求的回調任務,這種狀況就是then的註冊行爲會在狀態變化以前,因此須要將註冊回調函數緩存下來,等到異步任務執行時調用。

 1 function myPromise(excutor){
 2     var self = this;
 3     self.status = "pending";
 4     self.resolveValue = null; //緩存受理回調的參數
 5     self.rejectReason = null; //緩存拒絕回調的參數
 6     self.ResolveCallBackList = []; //當Promise是一個異步任務時,緩存受理回調函數
 7     self.RejectCallBackList = [];  //當Promise是一個異步任務時,緩存拒絕回調函數
 8 
 9     function resolve(value){
10         if(self.status === "pending"){
11             self.status = "Fulfilled";
12             self.resolveValue = value; // 將受理回調執行的參數緩存到Promise實例屬性上
13             self.ResolveCallBackList.forEach(function(ele){ 
14                 //這裏當excutor內是同步任務時,ResolveCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的受理回調函數
15                 ele();
16             });
17         }
18     }
19 
20     function reject(reason){
21         if(self.status === "pending"){
22             self.status = "Rejected";
23             self.rejectReason = reason; // 將拒絕回調執行的參數緩存到Promise實例屬性上
24             self.RejectCallBackList.forEach(function(ele){
25                 //這裏當excutor內是同步任務時,RejectCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的拒絕回調函數
26                 ele();
27             });
28         }
29     }
30     //當excutor拋出錯誤執行reject
31     try{
32         excutor(resolve,reject);
33     }catch(e){
34         reject(e);
35     }
36     
37 };
38 
39 myPromise.prototype.then = function(onFulfilled,onRejected){
40     var self = this;
41     if(self.status === "Fulfilled"){
42         onFulfilled(self.resolveValue);
43     }
44     if(self.status === "Rejected"){
45         onRejected(self.rejectReason);
46     }
47     // 當excutor執行時,內部回調是一個異步任務,Promise的狀態不會發生改變
48     // 因此異步做爲一個未來任務,先緩存到Promise實例對象上
49     if(self.status === "pending"){
50         self.ResolveCallBackList.push(function(){
51             onFulfilled(self.resolveValue);
52         });
53 
54         self.RejectCallBackList.push(function(){
55             onRejected(self.rejectReason);
56         })
57     }
58 }

測試代碼:(異步任務的出現異常報錯這部分還沒處理,因此只測試異步任務的受理或拒絕)

 1 var myP = new myPromise((resolve,reject) => {
 2     setTimeout(() => {
 3         // 測試resolve
 4         resolve("受理");
 5         // 測試reject
 6         // reject("拒絕");
 7     },1000);
 8 });
 9 myP.then((val) => {
10     console.log(val);
11 }, (reason) => {
12     console.log(reason);
13 });

3.then的鏈式調用:

在ES6的Promise中,then的鏈式調用是返回一個全新的Promise實例,這一點在前面的應用中已經有說明,鏈式調用中除了返回一個全新的Promise對象之外,還有一個關鍵的問題就是將前面的Promise的的返回值,做爲參數傳給後面一個Promise實例的回調函數使用。

這個階段暫時不處理返回Promise實例的相關內容,因此還記得我在使用的第一節第四小點,這裏測試第一個Promise實例的resolve和reject第二個then註冊受理和拒絕只會觸發受理。

因此這樣做爲一個基本鏈式調用實現就很是的簡單了,由於Promise實例化時須要執行一個同步的回調函數excutor,咱們都知道,then的回調註冊時同步進行,因此咱們只須要將then的註冊放到須要心生成的Promise實例化時同步執行excutor中,而後獲取前一個Promise的回調執行返回值,做爲新生成的Promise實例回調的參數傳入便可,這個提及來好像有點複雜,可是實現很是的簡單,建議直接看代碼:

 1 function myPromise(excutor){
 2     var self = this;
 3     self.status = "pending";
 4     self.resolveValue = null; //緩存受理回調的參數
 5     self.rejectReason = null; //緩存拒絕回調的參數
 6     self.ResolveCallBackList = []; //當Promise是一個異步任務時,緩存受理回調函數
 7     self.RejectCallBackList = [];  //當Promise是一個異步任務時,緩存拒絕回調函數
 8 
 9     function resolve(value){
10         if(self.status === "pending"){
11             self.status = "Fulfilled";
12             self.resolveValue = value; // 將受理回調執行的參數緩存到Promise實例屬性上
13             self.ResolveCallBackList.forEach(function(ele){ 
14                 //這裏當excutor內是同步任務時,ResolveCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的受理回調函數
15                 ele();
16             });
17         }
18     }
19 
20     function reject(reason){
21         if(self.status === "pending"){
22             self.status = "Rejected";
23             self.rejectReason = reason; // 將拒絕回調執行的參數緩存到Promise實例屬性上
24             self.RejectCallBackList.forEach(function(ele){
25                 //這裏當excutor內是同步任務時,RejectCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的拒絕回調函數
26                 ele();
27             });
28         }
29     }
30     //當excutor拋出錯誤執行reject
31     try{
32         excutor(resolve,reject);
33     }catch(e){
34         reject(e);
35     }
36     
37 };
38 
39 myPromise.prototype.then = function(onFulfilled,onRejected){
40     var self = this;
41 
42     var nextPromise = new myPromise(function (resolve,reject) {
43 
44         if(self.status === "Fulfilled"){
45                 var nextResolveValue = onFulfilled(self.resolveValue);
46                 resolve(nextResolveValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
47         }
48         if(self.status === "Rejected"){
49                 var nextRejectValue = onRejected(self.rejectReason);
50                 resolve(nextRejectValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
51         }
52         // 當excutor執行時,內部回調是一個異步任務,Promise的狀態不會發生改變
53         // 因此異步做爲一個未來任務,先緩存到Promise實例對象上
54         if(self.status === "pending"){
55             self.ResolveCallBackList.push(function(){
56                     var nextResolveValue = onFulfilled(self.resolveValue);
57                     resolve(nextResolveValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
58             });
59 
60             self.RejectCallBackList.push(function(){
61                     var nextRejectValue = onRejected(self.rejectReason);
62                     resolve(nextRejectValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
63             });
64         }
65     });
66     return nextPromise;
67 }

測試代碼:

 1 var myP = new myPromise((resolve,reject) => {
 2     setTimeout(() => {
 3         // 測試resolve
 4         // resolve(0);
 5         // 測試reject
 6         reject(0);
 7     },1000);
 8 });
 9 myP.then((val) => {
10     console.log("受理:" + val);
11     return 1;
12 }, (reason) => {
13     console.log("拒絕:" + reason);
14     return "1";
15 }).then((val) => {
16     console.log("受理:" + val);
17 }, (reason) => {
18     console.log("拒絕:" + reason);//這個暫時不會執行到
19 });

4.使用setTimeout模擬實現Promise異步回調任務:

注意這種模擬實現與ES6實現的異步回調有一個根本性差別,ES6的Promise異步回調任務是微任務,可是經過setTimeout模擬實現的是宏任務。實現其實也是很是的簡單,只須要將回調任務放到setTimeout的回調函數中便可,並設置延遲時間爲0;

而後再在這部分實現對回調任務中拋出錯誤的處理,這是由於回調任務中的錯誤須要在下一個Promise的reject中或者catch中被捕獲,因此有了鏈式調用的基礎就能夠來實現這個功能了。

還有第二節第一小點鐘提到忽略鏈式調用中的空then(),關於這個問題我前面只在使用中說會忽略這個空then,可是實際底層的實現並不是時忽略,而是將前一個Promise基於這個空的Promise實例傳遞給了下一個非空的Promise。這裏咱們先來看一段基於原生的Promise手動傳遞應用:

var myP = new Promise((resolve,reject) => {
    setTimeout(() => {
        // 測試resolve
        // resolve(0);
        // 測試reject
        reject(0);
    },1000);
});
myP.then((val) => {
    console.log("受理:" + val);
    return 1;
}, (reason) => {
    console.log("拒絕:" + reason);
    // 測試Error
    throw new error("這裏拋出錯誤");
}).then((val) => {
    return val; //將前一個受理的返回值傳遞給下一個Promise受理回調
},(reason) => {
    throw new Errror(reason); //將前一個Promise拋出的錯誤傳遞給下一個Promise的拒絕回調
})
.then((val) => {
    console.log("受理:" + val);
}, (reason) => {
    console.log("拒絕:" + reason);//這個暫時不會執行到
});

實際上,Promise底層也是基於這樣的傳遞行爲來處理空then的,並且在前面的Promise應用介紹中,有一種狀況沒有深刻的說明,就是當then(null,(...) => {...})、then((...) => {...},null)、then(null,null)進行深刻的說明,請示本質上一樣是使用了上面示例中的傳遞行爲。仍是那句話,提及來很是複雜,實際代碼很是簡單:

  1 function myPromise(excutor){
  2     var self = this;
  3     self.status = "pending";
  4     self.resolveValue = null; //緩存受理回調的參數
  5     self.rejectReason = null; //緩存拒絕回調的參數
  6     self.ResolveCallBackList = []; //當Promise是一個異步任務時,緩存受理回調函數
  7     self.RejectCallBackList = [];  //當Promise是一個異步任務時,緩存拒絕回調函數
  8 
  9     function resolve(value){
 10         if(self.status === "pending"){
 11             self.status = "Fulfilled";
 12             self.resolveValue = value; // 將受理回調執行的參數緩存到Promise實例屬性上
 13             self.ResolveCallBackList.forEach(function(ele){ 
 14                 //這裏當excutor內是同步任務時,ResolveCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的受理回調函數
 15                 ele();
 16             });
 17         }
 18     }
 19 
 20     function reject(reason){
 21         if(self.status === "pending"){
 22             self.status = "Rejected";
 23             self.rejectReason = reason; // 將拒絕回調執行的參數緩存到Promise實例屬性上
 24             self.RejectCallBackList.forEach(function(ele){
 25                 //這裏當excutor內是同步任務時,RejectCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的拒絕回調函數
 26                 ele();
 27             });
 28         }
 29     }
 30     //當excutor拋出錯誤執行reject
 31     try{
 32         excutor(resolve,reject);
 33     }catch(e){
 34         reject(e);
 35     }
 36     
 37 };
 38 
 39 myPromise.prototype.then = function(onFulfilled,onRejected){
 40     if(!onFulfilled){ //當沒有傳入受理回調函數時,自動將參數傳遞給下一個Promise實例的受理函數做爲參數
 41         onFulfilled = function(val){
 42             return val;
 43         }
 44     }
 45     if(!onRejected){ //當沒有傳入拒絕回調函數時,自動將參數傳遞給下一個Promise實例的拒絕函數做爲參數
 46         // 可能這裏你會疑惑,爲何要使用拋出錯誤的方式傳遞
 47         // 前面已經說明過,拒絕回調只有在Promise實例化中調用了拒絕回調函數之外,只有拋出錯誤纔會會觸發下一個Promise實例的拒絕回調
 48         onRejected = function(reason){
 49             throw new Error(reason);
 50         }
 51     }
 52     var self = this;
 53 
 54     var nextPromise = new myPromise(function (resolve,reject) {
 55 
 56         if(self.status === "Fulfilled"){
 57             setTimeout(function(){    //使用setTimeout模擬實現異步回調
 58                 try{                //使用try...catch來捕獲回調任務的異常
 59                     var nextResolveValue = onFulfilled(self.resolveValue);
 60                     resolve(nextResolveValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
 61                 }catch(e){
 62                     reject(e);
 63                 }
 64                 
 65             },0);
 66         }
 67         if(self.status === "Rejected"){
 68             setTimeout(function(){
 69                 try{
 70                     var nextRejectValue = onRejected(self.rejectReason);
 71                     resolve(nextRejectValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
 72                 }catch(e){
 73                     reject(e);
 74                 }
 75                 
 76             },0);
 77         }
 78         // 當excutor執行時,內部回調是一個異步任務,Promise的狀態不會發生改變
 79         // 因此異步做爲一個未來任務,先緩存到Promise實例對象上
 80         if(self.status === "pending"){
 81             self.ResolveCallBackList.push(function(){
 82                 setTimeout(function(){
 83                     try{
 84                         var nextResolveValue = onFulfilled(self.resolveValue);
 85                         resolve(nextResolveValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
 86                     }catch(e){
 87                         reject(e);
 88                     }
 89                     
 90                 },0);
 91             });
 92 
 93             self.RejectCallBackList.push(function(){
 94                 setTimeout(function(){
 95                     try{
 96                         var nextRejectValue = onRejected(self.rejectReason);
 97                         resolve(nextRejectValue);//將獲取的前一個Promise回調任務的返回值傳給新生成的Promise實例的受理回調任務
 98                     }catch(e){
 99                         reject(e);
100                     }
101                     
102                 },0);
103             });
104         }
105     });
106     return nextPromise;
107 }

這部分功能已經很是接近原生Promise了,就不提供測試代碼了,下面直接進入第五階段。

5.實現回調函數返回Promise實例:

關於回調返回Promise實例與前面的空then的處理思路很是相識,在空then的狀況下咱們須要將值傳遞給下一個then生成的Promise實例。那回調返回Promise實例就是須要,將本來註冊給then自身生成的Promise實例的回調從新註冊給上一個Promise回調返回的Promise實例,實現代碼一樣很是簡單:

  1 function myPromise(excutor){
  2     var self = this;
  3     self.status = "pending";
  4     self.resolveValue = null; //緩存受理回調的參數
  5     self.rejectReason = null; //緩存拒絕回調的參數
  6     self.ResolveCallBackList = []; //當Promise是一個異步任務時,緩存受理回調函數
  7     self.RejectCallBackList = [];  //當Promise是一個異步任務時,緩存拒絕回調函數
  8 
  9     function resolve(value){
 10         if(self.status === "pending"){
 11             self.status = "Fulfilled";
 12             self.resolveValue = value; // 將受理回調執行的參數緩存到Promise實例屬性上
 13             self.ResolveCallBackList.forEach(function(ele){ 
 14                 //這裏當excutor內是同步任務時,ResolveCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的受理回調函數
 15                 ele();
 16             });
 17         }
 18     }
 19 
 20     function reject(reason){
 21         if(self.status === "pending"){
 22             self.status = "Rejected";
 23             self.rejectReason = reason; // 將拒絕回調執行的參數緩存到Promise實例屬性上
 24             self.RejectCallBackList.forEach(function(ele){
 25                 //這裏當excutor內是同步任務時,RejectCallBackList沒有元素,當excutor內是一個異步任務時就會執行then緩存的拒絕回調函數
 26                 ele();
 27             });
 28         }
 29     }
 30     //當excutor拋出錯誤執行reject
 31     try{
 32         excutor(resolve,reject);
 33     }catch(e){
 34         reject(e);
 35     }
 36     
 37 };
 38 
 39 //用來處理回調返回值的狀況:當返回值爲Promise時,將回調函數註冊到該Promise上,若是爲普通值直接執行回調函數
 40 function ResolutionRetrunPromise(returnValue,res,rej){
 41     if(returnValue instanceof myPromise){
 42         returnValue.then(function(val){
 43             res(val);
 44         },function(reason){
 45             rej(reason);
 46         });
 47     }else{
 48         res(returnValue);
 49     }
 50 }
 51 
 52 
 53 myPromise.prototype.then = function(onFulfilled,onRejected){
 54     if(!onFulfilled){ //當沒有傳入受理回調函數時,自動將參數傳遞給下一個Promise實例的受理函數做爲參數
 55         onFulfilled = function(val){
 56             return val;
 57         }
 58     }
 59     if(!onRejected){ //當沒有傳入拒絕回調函數時,自動將參數傳遞給下一個Promise實例的拒絕函數做爲參數
 60         // 可能這裏你會疑惑,爲何要使用拋出錯誤的方式傳遞
 61         // 前面已經說明過,拒絕回調只有在Promise實例化中調用了拒絕回調函數之外,只有拋出錯誤纔會會觸發下一個Promise實例的拒絕回調
 62         onRejected = function(reason){
 63             throw new Error(reason);
 64         }
 65     }
 66     var self = this;
 67 
 68     var nextPromise = new myPromise(function (resolve,reject) {
 69 
 70         if(self.status === "Fulfilled"){
 71             setTimeout(function(){    //使用setTimeout模擬實現異步回調
 72                 try{                //使用try...catch來捕獲回調任務的異常
 73                     var nextResolveValue = onFulfilled(self.resolveValue);
 74                     ResolutionRetrunPromise(nextResolveValue,resolve,reject);//使用回調返回值來處理下一個回調任務
 75                 }catch(e){
 76                     reject(e);
 77                 }
 78                 
 79             },0);
 80         }
 81         if(self.status === "Rejected"){
 82             setTimeout(function(){
 83                 try{
 84                     var nextRejectValue = onRejected(self.rejectReason);
 85                     ResolutionRetrunPromise(nextRejectValue,resolve,reject);
 86                 }catch(e){
 87                     reject(e);
 88                 }
 89                 
 90             },0);
 91         }
 92         // 當excutor執行時,內部回調是一個異步任務,Promise的狀態不會發生改變
 93         // 因此異步做爲一個未來任務,先緩存到Promise實例對象上
 94         if(self.status === "pending"){
 95             self.ResolveCallBackList.push(function(){
 96                 setTimeout(function(){
 97                     try{
 98                         var nextResolveValue = onFulfilled(self.resolveValue);
 99                         ResolutionRetrunPromise(nextResolveValue,resolve,reject);
100                     }catch(e){
101                         reject(e);
102                     }
103                     
104                 },0);
105             });
106 
107             self.RejectCallBackList.push(function(){
108                 setTimeout(function(){
109                     try{
110                         var nextRejectValue = onRejected(self.rejectReason);
111                         ResolutionRetrunPromise(nextRejectValue,resolve,reject);
112                     }catch(e){
113                         reject(e);
114                     }
115                     
116                 },0);
117             });
118         }
119     });
120     return nextPromise;
121 }

測試代碼:

 1 var mP = new myPromise((resolve,reject) => {
 2     // resolve("受理1");
 3     reject("拒絕1");
 4 });
 5 mP.then((val) => {
 6     console.log(val);
 7     return new myPromise((resolve,reject) => {
 8         // resolve("受理2");
 9         // reject("拒絕2");
10     });
11 },(reason) => {
12     console.log(reason);
13     return new myPromise((resolve,reject) => {
14         // resolve("受理2");
15         reject("拒絕2");
16     });
17 }).then((val) => {
18     console.log(val);
19 },(reason) => {
20     console.log(reason);
21 })

6.實現Promise靜態方法race、all:

 1 myPromise.race = function(promiseArr){
 2     return new myPromise(function(resolve,reject){
 3         promiseArr.forEach(function(promise,index){
 4             promise.then(resolve,reject);
 5         });
 6     });
 7 }
 8 
 9 //all經過給每一個Promise傳遞一個受理回調,這個回調負責獲取每一個受理函數的參數,並判斷是否所有受理,若是所有受理觸發all自身的受理回調
10 //另外將all的reject傳遞給每一個Promise的reject,只要任意一個觸發就完成all的拒絕回調
11 myPromise.all = function(promiseArr){
12     function gen(length,resolve){
13         var count = 0;
14         var values = [];
15         return function(i,value){
16             values[i] = value;
17             if(++count === length){
18                 resolve(values);
19             }
20         }
21     }
22     return new myPromise(function(resolve,reject){
23         let done = gen(promiseArr.length,resolve);
24         promiseArr.forEach(function(promise,index){
25             promise.then((val) => {
26                 done(index,val);
27             },reject);
28         })
29     })
30 }

7.實現Promise原型方法catch、finally、以及擴展一個deferred靜態方法

 1 //原型方法catch的實現
 2 myPromise.prototype.catch = function(onRejected){
 3     return this.then(null,onRejected);
 4 }
 5 // 原型方法finally
 6 myPromise.prototype.finally = function(fun){
 7     fun();
 8 }
 9 //擴展靜態方法deferred方法
10 myPromise.deferred = function(){
11     var defer = {};
12     defer.promise = new Promise((resolve,reject) => {
13         defer.resolve = resolve;
14         defer.reject = reject;
15     });
16     return defer;
17 }

(全文完)

相關文章
相關標籤/搜索