原文出自:https://www.pandashen.com
概述
Promise 是 js 異步編程的一種解決方案,避免了 “回調地獄” 給編程帶來的麻煩,在 ES6 中成爲了標準,這篇文章重點不是敘述 Promise 的基本用法,而是從零開始,手寫一版符合 Promise/A+ 規範的 Promise,如果想了解更多 Promise 的基本用法,可以看 異步發展流程 —— Promise 的基本使用 這篇文章。
<br/>
Promise 構造函數的實現
我們在使用 Promise 的時候其實是使用 new
關鍵字創建了一個 Promise 的實例,其實 Promise 是一個類,即構造函數,下面來實現 Promise 構造函數。
Promise/A+ 規範的內容比較多,詳情查看 https://promisesaplus.com/,我們在實現 Promise 邏輯時會根據實現的部分介紹相關的 Promise/A+ 規範內容。
在 Promise/A+ 規範中規定:
- 構造函數的參數爲一個名爲
executor
的執行器,即函數,在創建實例時該函數內部邏輯爲同步,即立即執行; -
executor
執行時的參數分別爲resolve
和reject
,一個爲成功時執行的函數,一個爲失敗時執行的函數; - 在
executor
執行時,一旦出現錯誤立即調用reject
函數,並設置錯誤信息給reason
屬性; - 每個 Promise 實例有三個狀態
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 生效
});
<br/>
實例方法的實現
1、then 方法的實現
沒有 Promise 之前在一個異步操作的回調函數中返回一個結果在輸入給下一個異步操作,下一個異步操作結束後需要繼續執行回調,就形成回調函數的嵌套,在 Promise 中,原來回調函數中的邏輯只需要調用當前 Promise 實例的 then
方法,並在 then
方法的回調中執行,改變了原本異步的書寫方式。
在 then 方法中涉及到的 Promise/A+ 規範:
- Promise 實例的
then
方法中有兩個參數,都爲函數,第一個參數爲成功的回調onFulfilled
,第二個參數爲失敗的回調onRejected
; - 當 Promise 內部執行
resolve
時,調用實例的then
方法執行成功的回調onFulfilled
,當 Promise 內部執行reject
或執行出錯時,調用實例的then
方法執行錯誤的回調onRejected
; -
then
方法需要支持異步,即如果resovle
或reject
執行爲異步時,then
方法的回調onFulfilled
或onRejected
需要在後面執行; - Promise 需要支持鏈式調用,Promise 實例調用
then
方法後需要返回一個新的 Promise 實例。如果then
的回調中有返回值且是一個 Promise 實例,則該 Promise 實例執行後成功或失敗的結果傳遞給下一個 Promise 實例的then
方法onFulfilled
(成功的回調)或onRejected
(失敗的回調)的參數,如果返回值不是 Promise 實例,直接將這個值傳遞給下一個 Promise 實例then
方法回調的參數,then
的回調如果沒有返回值相當於返回undefined
; - Promise 實例鏈式調用
then
時,當任何一個then
執行出錯,鏈式調用下一個then
時會執行錯誤的回調,錯誤的回調沒有返回值相當於返回了undefined
,再次鏈式調用then
時會執行成功的回調; - Promise 實例的鏈式調用支持參數穿透,即當上一個
then
沒有傳遞迴調函數,或參數爲null
時,需要後面調用的then
的回調函數來接收; -
executor
在 Promise 構造函數中執行時使用try...catch...
捕獲異常,但是內部執行的代碼有可能是異步的,所以需要在then
方法中使用try...catch...
再次捕獲; - Promise 實例的
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
來處理。
resolvePromise 函數有四個參數:
- promise2:
then
執行後返回的 Promise 實例; - x:
then
的回調返回的結果; - resolve:promise2 的
resolve
函數; - reject:promise2 的
reject
函數。
在 resolvePromise 函數中涉及到的 Promise/A+ 規範:
- 將每個 Promise 實例調用
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
2、catch 方法的實現
// 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
// 成功了
靜態方法的實現
1、Promise.resolve 方法的實現
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));
// 成功了
2、Promise.reject 方法的實現
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));
// 失敗了
3、Promise.all 方法的實現
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
4、Promise.race 方法的實現
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+ 規範
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+ 規範一條一條進行極端的驗證,當驗證通過後會在窗口中這一條對應的執行項前打勾,驗證不通過打叉,直到所有的規範都驗證完畢。