ES6入門八:Promise非同步程式設計與模擬實現原始碼
- Promise的基本使用入門:
- ——例項化promise物件與註冊回撥
- ——巨集任務與微任務的執行順序
- ——then方法的鏈式呼叫與丟擲錯誤(throw new Error)
- ——鏈式呼叫的返回值與傳值
- Promise的基本使用進階:
- ——then、catch、finally的使用
- ——all、race的使用
- Promise的實現目的
- ——鏈式呼叫解決回撥地獄
- ——非同步回撥現在與未來任務分離
- ——信任問題(控制反轉):呼叫過早、呼叫過晚(不被呼叫)、呼叫次數過少過多、未能傳遞環境和引數、吞掉出現的錯誤和異常
- Promise的實現原理與模擬實現原始碼
一、Promise的基本使用入門
1.Promise是什麼?
Promise是用來實現JS非同步管理的解決方案,通過例項化Promise物件來管理具體的JS非同步任務。
從Promise管理回撥狀態的角度來看,Promise又通常被稱為狀態機,在Promise擁有三種狀態:pending、fulfilled、rejected。
用Promise解決JS非同步執行的邏輯來理解,可以說Promise是一個未來的事件,也就是說Promise管理的任務並不是在JS的同步執行緒上立即執行,而是會等待同步執行緒的程式執行完以後才會執行。
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產生的非同步任務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 });
以上的示例出現的結果會有四種:
//情況一、二 受理:ok then2 受理:then1_resolve_ok / then2 拒絕:then1_resolve_no //情況三、四 拒絕:no then2 受理:then1_reject_ok / then2 拒絕:then1_reject_no
通過示例可以看到,當前面一個then的回撥返回值是一個Promise物件時,後面的then觸發的受理或者拒絕是根據前面返回的Promise物件觸發的受理或者拒絕來決定的。
二、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函式。
三、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原始碼部分。
四、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 }
(全文完)
&n