異步發展流程 —— 手寫一個符合 Promise/A+ 規範的 Promise

在這裏插入圖片描述


原文出自: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 執行時的參數分別爲 resolvereject,一個爲成功時執行的函數,一個爲失敗時執行的函數;
  • executor 執行時,一旦出現錯誤立即調用 reject 函數,並設置錯誤信息給 reason 屬性;
  • 每個 Promise 實例有三個狀態 pendingfulfilledrejected,默認狀態爲 pending
  • 狀態只能從 pendingfulfilled 或從 pendingrejected,且不可逆;
  • 執行 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;

上面構造函數中的 resolvereject 方法在執行的時候都進行了當前狀態的判斷,只有狀態爲 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 方法需要支持異步,即如果 resovlereject 執行爲異步時,then 方法的回調 onFulfilledonRejected 需要在後面執行;
  • 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
  • 如果 promise2x 爲同一個對象,由於 x 要將執行成功或失敗的結果傳遞 promise2then 方法回調的參數,因爲是同一個 Promise 實例,此時既不能成功也不能失敗(自己不能等待自己完成),造成循環引用,這種情況下規定應該拋出一個類型錯誤來回絕;
  • 如果 x 是一個對象或者函數且不是 null,就去取 xthen 方法,如果 x 是對象,防止 x 是通過 Object.defineProperty 添加 then 屬性,並添加 getset 監聽,如果在監聽中拋出異常,需要被捕獲到,x.then 是一個函數,就當作 x 是一個 Promise 實例,直接執行xthen 方法,執行成功就讓 promise2 成功,執行失敗就讓 promise2 失敗,如果 x.then 不是函數,則說明 x 爲普通值,直接調用 promise2resolve 方法將 x 傳入,不滿足條件說明該返回值就是一個普通值,直接執行 promise2resolve 並將 x 作爲參數傳入;
  • 如果每次執行 xthen 方法,回調中傳入的參數還是一個 Promise 實例,循環往復,需要遞歸 resolvePromise 進行解析;
  • 在遞歸的過程中存在內、外層同時調用了 resolvereject 的情況,應該聲明一個標識變量 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 方法一一進行驗證。

驗證異步調用 resolvereject

// 文件: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+ 規範一條一條進行極端的驗證,當驗證通過後會在窗口中這一條對應的執行項前打勾,驗證不通過打叉,直到所有的規範都驗證完畢。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章