Promise 是 js 異步編程的一種解決方案,避免了 「回調地獄」 給編程帶來的麻煩,在 ES6 中成爲了標準,這篇文章重點不是敘述 Promise 的基本用法,而是從零開始,手寫一版符合 Promise/A+ 規範的 Promise,若是想了解更多 Promise 的基本用法,能夠看 異步發展流程 —— Promise 的基本使用 這篇文章。npm
咱們在使用 Promise 的時候實際上是使用 new
關鍵字建立了一個 Promise 的實例,其實 Promise 是一個類,即構造函數,下面來實現 Promise 構造函數。編程
Promise/A+ 規範的內容比較多,詳情查看 https://promisesaplus.com/,咱們在實現 Promise 邏輯時會根據實現的部分介紹相關的 Promise/A+ 規範內容。數組
在 Promise/A+ 規範中規定:promise
executor
的執行器,即函數,在建立實例時該函數內部邏輯爲同步,即當即執行;executor
執行時的參數分別爲 resolve
和 reject
,一個爲成功時執行的函數,一個爲失敗時執行的函數;executor
執行時,一旦出現錯誤當即調用 reject
函數,並設置錯誤信息給 reason
屬性;pending
、fulfilled
和 rejected
,默認狀態爲 pending
;pending
到 fulfilled
或從 pending
到 rejected
,且不可逆;resolve
函數會使狀態從 pending
變化到 fulfilled
並將參數存入實例的 value
屬性中;reject
函數會使狀態從 pending
變化到 rejected
並將錯誤信息存入實例的 reason
屬性中。針對上面的 Promise/A+ 規範,Promise 構造函數代碼實現以下:瀏覽器
// promise.js -- Promise 構造函數 function Promise(executor) { var self = this; self.status = "pending"; // 當前 Promise 實例的狀態 self.value = undefined; // 當前 Promise 實例成功狀態下的值 self.reason = undefined; // 當前 Promise 實例失敗狀態的錯誤信息 self.onFulfilledCallbacks = []; // 存儲成功的回調函數的數組 self.onRejectedCallbacks = []; // 存儲失敗的回調函數的數組 // 成功的執行的函數 function resolve(value) { if (self.status === "pending") { self.status = "fulfilled"; self.value = value; // 每次調用 resolve 時,執行 onFulfilledCallbacks 內部存儲的全部的函數(在實現 then 方法中詳細說明) self.onFulfilledCallbacks.forEach(function(fn) { fn(); }); } } // 失敗執行的函數 function reject(reason) { if (self.status === "pending") { self.status = "rejected"; self.reason = reason; // 每次調用 reject 時,執行 onRejectedCallbacks 內部存儲的全部的函數(在實現 then 方法中詳細說明) self.onRejectedCallbacks.forEach(function(fn) { fn(); }); } } // 調用執行器函數 try { executor(resolve, reject); } catch (e) { // 若是執行器執行時出現錯誤,直接調用失敗的函數 reject(e); } } // 將本身的 Promise 導出 module.exports = Promise;
上面構造函數中的 resolve
和 reject
方法在執行的時候都進行了當前狀態的判斷,只有狀態爲 pending
時,才能執行判斷內部邏輯,當兩個函數有一個執行後,此時狀態發生變化,再執行另外一個函數時就不會經過判斷條件,即不會執行判斷內部的邏輯,從而實現了兩個函數只有一個執行判斷內部邏輯的效果,使用以下:異步
// verify-promise.js -- 驗證 promise.js 的代碼 // 引入本身的 Promise 模塊 // 由於都驗證代碼都寫在 verify-promise.js 文件中,後面就再也不引入了 const Promise = require("./promise.js"); let p = new Promise((resolve, reject) => { // ...同步代碼 resolve(); reject(); // 上面兩個函數只有先執行的 resolve 生效 });
沒有 Promise 以前在一個異步操做的回調函數中返回一個結果在輸入給下一個異步操做,下一個異步操做結束後須要繼續執行回調,就造成回調函數的嵌套,在 Promise 中,原來回調函數中的邏輯只須要調用當前 Promise 實例的 then
方法,並在 then
方法的回調中執行,改變了本來異步的書寫方式。異步編程
在 then 方法中涉及到的 Promise/A+ 規範:函數
then
方法中有兩個參數,都爲函數,第一個參數爲成功的回調 onFulfilled
,第二個參數爲失敗的回調 onRejected
;resolve
時,調用實例的 then
方法執行成功的回調 onFulfilled
,當 Promise 內部執行 reject
或執行出錯時,調用實例的 then
方法執行錯誤的回調 onRejected
;then
方法須要支持異步,即若是 resovle
或 reject
執行爲異步時,then
方法的回調 onFulfilled
或 onRejected
須要在後面執行;then
方法後須要返回一個新的 Promise 實例。若是 then
的回調中有返回值且是一個 Promise 實例,則該 Promise 實例執行後成功或失敗的結果傳遞給下一個 Promise 實例的 then
方法 onFulfilled
(成功的回調)或 onRejected
(失敗的回調)的參數,若是返回值不是 Promise 實例,直接將這個值傳遞給下一個 Promise 實例 then
方法回調的參數,then
的回調若是沒有返回值至關於返回 undefined
;then
時,當任何一個 then
執行出錯,鏈式調用下一個 then
時會執行錯誤的回調,錯誤的回調沒有返回值至關於返回了 undefined
,再次鏈式調用 then
時會執行成功的回調;then
沒有傳遞迴調函數,或參數爲 null
時,須要後面調用的 then
的回調函數來接收;executor
在 Promise 構造函數中執行時使用 try...catch...
捕獲異常,可是內部執行的代碼有多是異步的,因此須要在 then
方法中使用 try...catch...
再次捕獲;then
方法中的回調爲 micro-tasks
(微任務),回調內的代碼應晚於同步代碼執行,在瀏覽器內部調用微任務接口,咱們這裏模擬使用宏任務代替。針對上面的 Promise/A+ 規範,then 方法代碼實現以下:測試
// promise.js -- then 方法 Promise.prototype.then = function(onFulfilled, onRejected) { // 實現參數穿透 if(typeof onFulfilled !== "function") { onFulfilled = function (data) { return data; } } if(typeof onRejected !== "function") { onRejected = function (err) { throw err; } } // 返回新的 Promise,規範中規定這個 Promise 實例叫 promise2 var promise2 = new Promise(function (resolve, reject) { if (this.status === "fulfilled") { // 用宏任務替代模擬微任務,目的是使 `then` 的回調晚於同步代碼執行 setTimeout(function () { try { // 捕獲異步的異常 // onFulfilled 執行完返回值的處理,x 爲成功回調的返回值 var x = onFulfilled(this.value); // 處理返回值單獨封裝一個方法 resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); } if (this.status === "rejected") { setTimeout(function () { try { // onRejected 執行完返回值的處理,x 爲失敗回調的返回值 var x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); } // 若是在 Promise 執行 resolve 或 renject 爲異步 // 將 then 的執行程序存儲在實例對應的 onFulfilledCallbacks 或 onRejectedCallbacks 中 if (this.status === "pending") { this.onFulfilledCallbacks.push(function() { setTimeout(function () { try { var x = onFulfilled(this.value); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); }); this.onRejectedCallbacks.push(function() { setTimeout(function () { try { var x = onRejected(this.reason); resolvePromise(promise2, x, resolve, reject); } catch (e) { reject(e); } }.bind(this), 0); }); } }); return promise2; };
在處理 then
回調的返回值時,其實就是在處理該返回值與 then
方法在執行後返回的新 Promise 實例(即 promise2)之間的關係,由於不管 Promise 的執行器在執行 resolve
仍是 reject
是同步或是異步,都須要進行處理,因此咱們單獨封裝一個函數 resolvePromise
來處理。ui
resolvePromise 函數有四個參數:
then
執行後返回的 Promise 實例;then
的回調返回的結果;resolve
函數;reject
函數。在 resolvePromise 函數中涉及到的 Promise/A+ 規範:
then
後返回的新 Promise 實例稱爲 promise2
,將 then
回調返回的值稱爲 x
;promise2
和 x
爲同一個對象,因爲 x
要將執行成功或失敗的結果傳遞 promise2
的 then
方法回調的參數,由於是同一個 Promise 實例,此時既不能成功也不能失敗(本身不能等待本身完成),形成循環引用,這種狀況下規定應該拋出一個類型錯誤來回絕;x
是一個對象或者函數且不是 null
,就去取 x
的 then
方法,若是 x
是對象,防止 x
是經過 Object.defineProperty
添加 then
屬性,並添加 get
和 set
監聽,若是在監聽中拋出異常,須要被捕獲到,x.then
是一個函數,就看成 x
是一個 Promise 實例,直接執行x
的 then
方法,執行成功就讓 promise2
成功,執行失敗就讓 promise2
失敗,若是 x.then
不是函數,則說明 x
爲普通值,直接調用 promise2
的 resolve
方法將 x
傳入,不知足條件說明該返回值就是一個普通值,直接執行 promise2
的 resolve
並將 x
做爲參數傳入;x
的 then
方法,回調中傳入的參數仍是一個 Promise 實例,循環往復,須要遞歸 resolvePromise
進行解析;resolve
和 reject
的狀況,應該聲明一個標識變量 called
作判斷來避免這種狀況。針對上面的 Promise/A+ 規範,resolvePromise 函數代碼實現以下:
// promise.js -- resolvePromise 方法 function resolvePromise(promise2, x, resolve, reject) { // 判斷 x 和 promise2 是否是同一個函數 if (promise2 === x) { reject(new TypeError("循環引用")); } // x 是對象或者函數而且不是 null,若是不知足該條件說明 x 只是一個普通的值 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 標識變量,防止遞歸內外層 resolve 和 reject 同時調用 // 針對 Promise,x 爲普通值的時候能夠放行 var called; // 爲了捕獲 Object.defineProperty 建立的 then 屬性時添加監聽所拋出的異常 try { var then = x.then; if (typeof then === "function") { // then 爲一個方法,就看成 x 爲一個 promise // 執行 then,第一個參數爲 this(即 x),第二個參數爲成功的回調,第三個參數爲失敗的回調 then.call(x, function (y) { if (called) return; called = true; // 若是 y 是 Promise 就繼續遞歸解析 resolvePromise(promise2, y, resolve, reject); }, function (err) { if (called) return; called = true; reject(err); }); } else { // x 是一個普通對象,直接成功便可 resolve(x); } } catch(e) { if (called) return; called = true; reject(e); } } else { resolve(x); } }
上面咱們按照 Promise/A+ 規範實現了 Promise 的 then
方法,接下來針對上面的規範,用一些有針對行的案例來對 then
方法一一進行驗證。
驗證異步調用 resolve
或 reject
:
// 文件:verify-promise.js // 驗證 promise.js 異步調用 resolve 或 reject let p = new Promise((resolve, reject) => { setTimeout(() => resolve(), 1000); }); p.then(() => console.log("執行了")); // 執行了
驗證鏈式調用 then
返回 Promise 實例:
// 文件:verify-promise.js // 驗證 promise.js then 回調返回 Promise 實例 let p1 = new Promise((resolve, reject) => resolve()); let p2 = new Promise((resolve, reject) => resolve("hello")); p1.then(() => p2).then(data => console.log(data)); // hello
驗證鏈式調用 then
返回普通值:
// 文件:verify-promise.js // 驗證 promise.js then 回調返回普通值 let p = new Promise((resolve, reject) => resolve()); p.then(() => "hello").then(data => console.log(data)); // hello
驗證鏈式調用 then
中執行出錯鏈式調用 then
執行錯誤的回調後,再次鏈式調用 then
:
// 文件:verify-promise.js // 驗證 promise.js 鏈式調用 then 中執行出錯鏈式調用 then 執行錯誤的回調後,再次鏈式調用 then let p = new Promise((resolve, reject) => resolve()); p.then(() => { throw new Error("error"); }).then(() => { console.log("success"); }, err => { console.log(err); }).then(() => { console.log("成功"); }, () => { console.log("失敗"); }); // Error: error at p.then... // 成功
驗證 then
的參數穿透:
// 文件:verify-promise.js // 驗證 then 的參數穿透 let p1 = new Promise((resolve, reject) => resolve("ok")); let p2 = p1.then().then(data => { console.log(data); throw new Error("出錯了"); }); p2.then().then(null, err => console.log(err)); // ok // 出錯了
驗證 then
方法是否晚於同步代碼執行:
// 文件:verify-promise.js // 驗證 then 方法是否晚於同步代碼執行 let p = new Promise((resolve, reject) => { resolve(1); }); p.then(data => console.log(data)); console.log(2); // 2 // 1
驗證循環引用:
// 文件:verify-promise.js // 驗證 promise.js 循環引用 let p1 = new Promise((resolve, reject) => resolve()); // 讓 p1 then 方法的回調返回本身 var p2 = p1.then(() => { return p2; }); p2.then(() => { console.log("成功"); }, err => { console.log(err); }); // TypeError: 循環引用 at resolvePromise...
驗證 then
回調返回對象經過 Object.definePropertype
添加 then
屬性並添加 get
監聽,在觸發監聽時拋出異常:
// 文件:verify-promise.js // 驗證 promise.js then 回調返回對象經過 Object.definePropertype 添加 then 和 get 監聽,捕獲異常 let obj = {}; Object.defineProperty(obj, "then", { get () { throw new Error(); } }); let p = new Promise((resolve, reject) => resolve()); p.then(() => { return obj; }).then(() => { console.log("成功"); }, () => { console.log("出錯了"); }); // 出錯了
驗證每次執行 resolve
都傳入 Promise 實例,須要將最終的執行結果傳遞給下一個 Promise 實例 then
的回調中:
// 文件:verify-promise.js // 驗證 promise.js 每次執行 resolve 都傳入 Promise 實例 let p = new Promise((resolve, reject) => resolve()); p.then(() => { return new Promise((resolve, reject) => { resolve(new Promise(resolve, reject) => { resolve(new Promise(resolve, reject) => { resolve(200); }); }); }); }).then(data => { console.log(data); }); // 200
// promise.js -- catch 方法 Promise.prototype.catch = function (onRejected) { return this.then(null, onRejected); }
catch
方法能夠理解爲是 then
方法的一個簡寫,只是參數中少了成功的回調,因此利用 Promise/A+ 規範中參數穿透的特性,很容易就實現了 catch
方法,catch
方法的真相就是這麼的簡單。
驗證 catch
方法:
// 文件:verify-promise.js // 驗證 promise.js 的 catch 方法 let p = new Promise((resolve, reject) => reject("err")); p.then().catch(err => { console.log(err); }).then(() => { console.log("成功了"); }); // err // 成功了
Promise.resolve
方法傳入一個參數,並返回一個新的 Promise 實例,這個參數做爲新 Promise 實例 then
方法成功回調的參數,在調用時感受直接成功了,實際上是直接執行了返回 Promise 實例的 resolve
。
// promise.js -- Promise.resolve 方法 Promise.resolve = function (val) { return new Promise(function (resolve, reject) { resolve(val); }); }
驗證 Promise.resolve
方法:
// 文件:verify-promise.js // 驗證 promise.js 的 Promise.resolve 方法 Promise.resolve("成功了").then(data => console.log(data)); // 成功了
Promise.reject
方法與 Promise.resolve
的實現思路相同,不一樣的是,直接調用了返回新 Promise 實例的 reject
。
// promise.js -- Promise.reject 方法 Promise.reject = function (reason) { return new Promise(function (resolve, reject) { reject(reason); }); }
驗證 Promise.reject
方法:
// 文件:verify-promise.js // 驗證 promise.js 的 Promise.reject 方法 Promise.reject("失敗了").then(err => console.log(err)); // 失敗了
Promise.all
方法能夠實現多個 Promise 實例的並行,返回值爲一個新的 Promise 實例,當全部結果都爲成功時,返回一個數組,該數組存儲的爲每個 Promise 實例的返回結果,這些 Promise 實例的返回順序前後不肯定,可是返回值的數組內存儲的返回結果是按照數組中 Promise 實例最初順序進行排列的,返回的數組做爲返回 Promise 實例成功回調的參數,當其中一個失敗,直接返回錯誤信息,並做爲返回 Promise 實例失敗回調的參數。
// promise.js -- Promise.all 方法 Promise.all = function (promises) { return new Promise(function (resolve, reject) { // 存儲返回值 var result = []; // 表明存入的個數,由於 Promise 爲異步,不知道哪一個 Promise 先成功,不能用數組的長度來判斷 var idx = 0; // 用來構建所有成功的返回值 function processData(index, data) { result[index] = data; // 將返回值存入數組 idx++; if (idx === promises.length) { resolve(result); } } for(var i = 0; i < promises.length; i++) { // 由於 Primise 爲異步,保證 i 值是順序傳入 (function (i) { promises[i].then(function (data) { processData(i, data); }, reject); })(i); } }); }
驗證 Promise.all
方法:
// 文件:verify-promise.js // 驗證 promise.js 的 Promise.all 方法 let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)); Promise.all([p1, p2]).then(data => console.log(data)); // [1, 2] let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p4 = new Promise((resolve, reject) => setTimeout(() => reject(2), 1000)); Promise.all([p3, p4]).then(data => { console.log(data); }).catch(err => { console.log(err); }); // 2
Promise.race
方法與 Promise.all
相似,一樣能夠實現多個 Promise 實例的並行,一樣返回值爲一個新的 Promise 實例,參數一樣爲一個存儲多個 Promise 實例的數組,區別是隻要有一個 Promise 實例返回結果,不管成功或失敗,則直接返回這個結果,並做爲新 Promise 實例 then
方法中成功或失敗的回調函數的參數。
// promise.js -- Promise.race 方法 Promise.race = function (promises) { return new Promise(function (resolve, reject) { for(var i = 0; i < promises.length; i++) { promises[i].then(resolve, reject); } }); }
驗證 Promise.race
方法:
// 文件:verify-promise.js // 驗證 promise.js 的 Promise.race 方法 let p1 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p2 = new Promise((resolve, reject) => setTimeout(() => resolve(2), 1000)); Promise.race([p1, p2]).then(data => console.log(data)); // 2 let p3 = new Promise((resolve, reject) => setTimeout(() => resolve(1), 2000)); let p4 = new Promise((resolve, reject) => setTimeout(() => reject(2), 1000)); Promise.all([p3, p4]).then(data => { console.log(data); }).catch(err => { console.log(err); }); // 2
promises-aplus-test
是專門用來驗證 Promise 代碼是否符合 Promise/A+ 規範的包,須要經過 npm
下載。
npm install promises-aplus-test -g
測試方法:
promise.js
中寫入測試代碼;promises-aplus-test
+ fileName
。測試代碼:
// promise.js -- 測試方法 Promise.derfer // Promise 語法糖 // 好處:解決 Promise 嵌套問題 // 壞處:錯誤處理不方便 Promise.derfer = Promise.deferred = function () { let dfd = {}; dfd.promise = new Promise((resolve, reject) => { dfd.resolve = resolve; dfd.reject = reject; }); return dfd; }
輸入命令:
promises-aplus-test promise.js
執行上面命令後,會根據 Promise/A+ 規範一條一條進行極端的驗證,當驗證經過後會在窗口中這一條對應的執行項前打勾,驗證不經過打叉,直到全部的規範都驗證完畢。