「一次寫過癮」手寫 Promise 全家桶 + Generator + async/await

2021-03-02 全棧前端精選

(給全棧前端精選加星標,提升前端技能)

手寫 Promise 全家桶

Promise/A+ 規範[4]鎮樓!

如果你沒讀過 Promise/A+ 規範也沒關係,我幫你總結了如下三部分重點:

不過建議看完本文後還是要親自去讀一讀,不多 bb,開始展示。

規範重點1.Promise 狀態

Promise 的三個狀態分別是 pending、fulfilled 和 rejected。

pending: 待定,Promise 的初始狀態。在此狀態下可以落定 (settled) 為 fulfilled 或 rejected狀態。fulfilled: 兌現(解決),表示執行成功。Promise 被 resolve 後的狀態,狀態不可再改變,且有一個私有的值 value。rejected: 拒絕,表示執行失敗。Promise 被 reject 後的狀態,狀態不可再改變,且有一個私有的原因 reason。

注意:value 和 reason 也是不可變的,它們包含原始值或對象的不可修改的引用,默認值為 undefined。

2.Then 方法

要求必須提供一個 then 方法來訪問當前或最終的 value 或 reason。

promise.then(onFulfilled, onRejected)

1.then 方法接受兩個函數作為參數,且參數可選。3.兩個函數都是異步執行,會放入事件隊列等待下一輪 tick。4.當調用 onFulfilled 函數時,會將當前 Promise 的 value 值作為參數傳入。5.當調用 onRejected 函數時,會將當前 Promise 的 reason 失敗原因作為參數傳入。7.then 可以被同一個 Promise 多次調用。3.Promise 解決過程

Promise 的解決過程是一個抽象操作,接收一個 Promise 和一個值 x。

針對 x 的不同值處理以下幾種情況:

拋出 TypeError 錯誤,拒絕 Promise。

如果 x 處於待定狀態,那麼 Promise 繼續等待直到 x 兌現或拒絕,否則根據 x 的狀態兌現/拒絕 Promise。

取出 x.then 並調用,調用時將 this 指向 x。將 then 回調函數中得到的結果 y 傳入新的 Promise 解決過程中,遞歸調用。

如果執行報錯,則將以對應的失敗原因拒絕 Promise。

這種情況就是處理擁有 then() 函數的對象或函數,我們也叫它 thenable。

以 x 作為值執行 Promise。

手寫 Promise1.首先定義 Promise 的三個狀態
var PENDING = 'pending';
var FULFILLED = 'fulfilled';
var REJECTED = 'rejected';

2.我們再來搞定 Promise 的構造函數

創建 Promise 時需要傳入 execute 回調函數,接收兩個參數,這兩個參數分別用來兌現和拒絕當前 Promise。

所以我們需要定義 resolve() 和 reject() 函數。

初始狀態為 PENDING,在執行時可能會有返回值 value,在拒絕時會有拒絕原因 reason。

同時需要注意,Promise 內部的異常不能直接拋出,需要進行異常捕獲。

function Promise(execute) {
    var that = this;
    that.state = PENDING;
    function resolve(value) {
        if (that.state === PENDING) {
            that.state = FULFILLED;
            that.value = value;
        }
    }
    function reject(reason) {
        if (that.state === PENDING) {
            that.state = REJECTED;
            that.reason = reason;
        }
    }
    try {
        execute(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

3. 實現 then() 方法

then 方法用來註冊當前 Promise 狀態落定後的回調,每個 Promise 實例都需要有它,顯然要寫到 Promise 的原型 prototype 上,並且 then() 函數接收兩個回調函數作為參數,分別是 onFulfilled 和 onRejected。

Promise.prototype.then = function(onFulfilled, onRejected) {}

根據上面第 2 條規則,如果可選參數不為函數時應該被忽略,我們要對參數進行如下判斷。

onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : function(x) { return x; }
onRejected = typeof onRejected === 'function' ? onRejected : function(e) { throw e; }

根據第 3 條規則,需要使用 setTimeout 延遲執行,模擬異步。

根據第 4 條、第 5 條規則,需要根據 Promise 的狀態來執行對應的回調函數。

在 PENDING 狀態時,需要等到狀態落定才能調用。我們可以將 onFulfilled 和 onRejected 函數存到 Promise 的屬性 onFulfilledFn 和 onRejectedFn 中,

當狀態改變時分別調用它們。

var that = this;
var promise;
if (that.state === FULFILLED) {
    setTimeout(function() {
        onFulfilled(that.value);
    });
}
if (that.state === REJECTED) {
    setTimeout(function() {
        onRejected(that.reason);
    });
}
if (that.state === PENDING) {
     that.onFulfilledFn = function() {
        onFulfilled(that.value);
    }
    that.onRejectedFn = function() {
        onRejected(that.reason);
    }
}

根據第 6 條規則,then 函數的返回值為 Promise,我們分別給每個邏輯添加並返回一個 Promise。

同時,then 支持鏈式調用,我們需要將 onFulfilledFn 和 onRejectedFn 改成數組。

var that = this;
var promise;
if (that.state === FULFILLED) {
    promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            try {
                onFulfilled(that.value);
            } catch (reason) {
                reject(reason);
            }
        });
    });
}
if (that.state === REJECTED) {
    promise = new Promise(function(resolve, reject) {
        setTimeout(function() {
            try {
                onRejected(that.reason);
            } catch (reason) {
                reject(reason);
            }
        });
    });
}
if (that.state === PENDING) {
    promise = new Promise(function(resolve, reject) {
        that.onFulfilledFn.push(function() {
            try {
                onFulfilled(that.value);
            } catch (reason) {
                reject(reason);
            }
        })
        that.onRejectedFn.push(function() {
            try {
                onRejected(that.reason);
            } catch (reason) {
                reject(reason);
            }
        });
    });
}

與上面相對應的,再將 Promise 的構造函數相應的進行改造。

1.添加 onFulFilledFn 和 onRejectedFn 數組。

2.resolve() 和 reject() 函數改變狀態時,需要異步調用數組中的函數,同樣使用 setTimeout 來模擬異步。

function Promise(execute) {
    var that = this;
    that.state = PENDING;
    that.onFulfilledFn = [];
    that.onRejectedFn = [];

    function resolve(value) {
        setTimeout(function() {
            if (that.state === PENDING) {
                that.state = FULFILLED;
                that.value = value;
                that.onFulfilledFn.forEach(function(fn) {
                    fn(that.value);
                })
            }
        })
    }
    function reject(reason) {
        setTimeout(function() {
            if (that.state === PENDING) {
                that.state = REJECTED;
                that.reason = reason;
                that.onRejectedFn.forEach(function(fn) {
                    fn(that.reason);
                })
            }
        })
    }
    try {
        execute(resolve, reject);
    } catch (e) {
        reject(e);
    }
}

4.Promise 解決過程 resolvePromise()

Promise 解決過程分為以下幾種情況,我們需要分別進行處理:

1.x 等於 Promise TypeError 錯誤

此時相當於 Promise.then 之後 return 了自己,因為 then 會等待 return 後的 Promise,導致自己等待自己,一直處於等待。。

function resolvePromise(promise, x) {
    if (promise === x) {
        return reject(new TypeError('x 不能等於 promise'));
    }
}

2.x 是 Promise 的實例

如果 x 處於待定狀態,Promise 會繼續等待直到 x 兌現或拒絕,否則根據 x 的狀態兌現/拒絕 Promise。

我們需要調用 Promise 在構造時的函數 resolve() 和 reject() 來改變 Promise 的狀態。

function resolvePromise(promise, x, resolve, reject) {
    // ...
    if (x instanceof Promise) {
        if (x.state === FULFILLED) {
            resolve(x.value);
        } else if (x.state === REJECTED) {
            reject(x.reason);
        } else {
            x.then(function(y) {
                resolvePromise(promise, y, resolve, reject);
            }, reject);
        }
    }
}

3.x 是對象或函數

取出 x.then 並調用,調用時將 this 指向 x,將 then 回調函數中得到的結果 y 傳入新的 Promise 解決過程中,遞歸調用。

如果執行報錯,則將以對應的失敗原因拒絕 Promise。

x 可能是一個 thenable 而非真正的 Promise。

需要設置一個變量 executed 避免重複調用。

function resolvePromise(promise, x, resolve, reject) {
    // ...
    if ((x !== null) && ((typeof x === 'object' || (typeof x === 'function'))) {
        var executed;
        try {
            var then = x.then;
            if (typeof then === 'function') {
                then.call(x, function(y) {
                    if (executed) return;
                    executed = true;
                    return resolvePromise(promise, y, resolve, reject);
                }, function (e) {
                    if (executed) return;
                    executed = true;
                    reject(e);
                }) 
            } else {
                resolve(x);
            }
        } catch (e) {
            if (executed) return;
            executed = true;
            reject(e);
        }
    }
}

4.直接將 x 作為值執行

function resolvePromise(promise, x, resolve, reject) {
    // ...
    resolve(x)
}

測試
// 為了支持測試,將模塊導出
module.exports = {
  deferred() {
    var resolve;
    var reject;
    var promise = new Promise(function (res, rej) {
      resolve = res;
      reject = rej;
    })
    return {
      promise,
      resolve,
      reject
    }
  }
}

我們可以選用這款測試工具對我們寫的 Promise 進行測試 Promise/A+ 測試工具: promises-aplus-tests[5]。

目前支持 827 個測試用例,我們只需要在導出模塊的時候遵循 CommonJS 規範,按照要求導出對應的函數即可。

Promise.resolve

Promise.resolve() 可以實例化一個解決(fulfilled) 的 Promise。

Promise.resolve = function(value) {
    if (value instanceof Promise) {
        return value;
    }

    return new Promise(function(resolve, reject) {
        resolve(value);
    });
}

Promise.reject

Promise.reject() 可以實例化一個 rejected 的 Promise 並拋出一個異步錯誤(這個錯誤不能通過try/catch捕獲,只能通過拒絕處理程序捕獲)

Promise.reject = function(reason) {
    return new Promise(function(resolve, reject) {
        reject(reason);
    });
}

Promise.prototype.catch

Promise.prototype.catch() 方法用於給 Promise 添加拒絕時的回調函數。

Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected);
}

Promise.prototype.finally

Promise.prototype.finally() 方法用於給 Promise 添加一個不管最終狀態如何都會執行的操作。

Promise.prototype.finally = function(fn) {
    return this.then(function(value) {
        return Promise.resolve(value).then(function() {
            return value;
        });
    }, function(error) {
        return Promise.resolve(reason).then(function() {
            throw error;
        });
    });
}

Promise.all

Promise.all() 方法會將多個 Promise 實例組合成一個新的 Promise 實例。

組合後的 Promise 實例只有當每個包含的 Promise 實例都解決(fulfilled)後才解決(fulfilled),如果有一個包含的 Promise 實例拒絕(rejected)了,則合成的 Promise 也會拒絕(rejected)。

兩個注意點:

傳入的是可迭代對象,用 for...of 遍歷 Iterable 更安全傳入的每個實例不一定是 Promise,需要用 Promise.resolve() 包裝
Promise.all = function(promiseArr) {
    return new Promise(function(resolve, reject) {
        const length = promiseArr.length;
        const result = [];
        let count = 0;
        if (length === 0) {
            return resolve(result);
        }

        for (let item of promiseArr) {
            Promise.resolve(item).then(function(data) {
                result[count++] = data;
                if (count === length) {
                    resolve(result);
                }
            }, function(reason) {
                reject(reason);
            });
        }
    });
}

Promise.race

Promise.race() 同樣返回一個合成的 Promise 實例,其會返回這一組中最先解決(fulfilled)或拒絕(rejected)的 Promise 實例的返回值。

Promise.race = function(promiseArr) {
    return new Promise(function(resolve, reject) {
        const length = promiseArr.length;
        if (length === 0) {
            return resolve();
        } 

        for (let item of promiseArr) {
            Promise.resolve(item).then(function(value) {
                return resolve(value);
            }, function(reason) {
                return reject(reason);
            });
        }
    });
}

Promise.any

Promise.any() 相當於 Promise.all() 的反向操作,同樣返回一個合成的 Promise 實例,只要其中包含的任何一個 Promise 實例解決(fulfilled)了,合成的 Promise 就解決(fulfilled)。

只有當每個包含的 Promise 都拒絕(rejected)了,合成的 Promise 才拒絕(rejected)。

Promise.any = function(promiseArr) {
    return new Promise(function(resolve, reject) {
        const length = promiseArr.length;
        const result = [];
        let count = 0;
        if (length === 0) {
            return resolve(result);
        } 

        for (let item of promiseArr) {
            Promise.resolve(item).then((value) => {
                return resolve(value);
            }, (reason) => {
                result[count++] = reason;
                if (count === length) {
                    reject(result);
                }
            });
        }
    });
}

Promise.allSettled

Promise.allSettled() 方法也是返回一個合成的 Promise,不過只有等到所有包含的每個 Promise 實例都返回結果落定時,不管是解決(fulfilled)還是拒絕(rejected),合成的 Promise 才會結束。一旦結束,狀態總是 fulfilled。

其返回的是一個對象數組,每個對象表示對應的 Promise 結果。

對於每個結果對象,都有一個 status 字符串。如果它的值為 fulfilled,則結果對象上存在一個 value 。如果值為 rejected,則存在一個 reason 。

Promise.allSettled = function(promiseArr) {
  return new Promise(function(resolve) {
    const length = promiseArr.length;
    const result = [];
    let count = 0;

    if (length === 0) {
      return resolve(result);
    } else {
      for (let item of promiseArr) {
        Promise.resolve(item).then((value) => {
            result[count++] = { status: 'fulfilled', value: value };
            if (count === length) {
                return resolve(result);
            }
        }, (reason) => {
            result[count++] = { status: 'rejected', reason: reason };
            if (count === length) {
                return resolve(result);
            }
        });
      }
    }
  });
}


// 使用 Promise.finally 實現
Promise.allSettled = function(promises) {
    // 也可以使用擴展運算符將 Iterator 轉換成數組
    // const promiseArr = [...promises]
    const promiseArr = Array.from(promises)
    return new Promise(resolve => {
        const result = []
        const len = promiseArr.length;
        let count = len;
        if (len === 0) {
          return resolve(result);
        }
        for (let i = 0; i < len; i++) {
            promiseArr[i].then((value) => {
                result[i] = { status: 'fulfilled', value: value };
            }, (reason) => {
                result[i] = { status: 'rejected', reason: reason };
            }).finally(() => { 
                if (!--count) {
                    resolve(result);
                }
            });
        }
    });
}



// 使用 Promise.all 實現
Promise.allSettled = function(promises) {
    // 也可以使用擴展運算符將 Iterator 轉換成數組
    // const promiseArr = [...promises]
    const promiseArr = Array.from(promises)
    return Promise.all(promiseArr.map(p => Promise.resolve(p).then(res => {
      return { status: 'fulfilled', value: res }
    }, error => {
      return { status: 'rejected', reason: error }
    })));
};

手寫 Generator 函數

先來簡單回顧下 Generator 的使用:

function* webCanteenGenerator() {
    yield '店小二兒,給我切兩斤牛肉來';
    yield '再來十八碗酒';
    return '好酒!這酒有力氣!';
}

var canteen = webCanteenGenerator();
canteen.next();
canteen.next();
canteen.next();
canteen.next();

// {value: "店小二兒,給我切兩斤牛肉來", done: false}
// {value: "再來十八碗酒", done: false}
// {value: "好酒!這酒有力氣!", done: true}
// {value: undefined, done: true}

// 簡易版
// 定義生成器函數,入參是任意集合
function webCanteenGenerator(list) {
    var index = 0;
    var len = list.length;
    return {
        // 定義 next 方法
        // 記錄每次遍歷位置,實現閉包,藉助自由變量做迭代過程中的「遊標」
        next: function() {
            var done = index >= len; // 如果索引還沒有超出集合長度,done 為 false
            var value = !done ? list[index++] : undefined; // 如果 done 為 false,則可以繼續取值
            // 返回遍歷是否完畢的狀態和當前值
            return {
                done: done,
                value: value
            }
        }
    }
}

var canteen = webCanteenGenerator(['道路千萬條', '安全第一條', '行車不規範']);
canteen.next();
canteen.next();
canteen.next();

// {done: false, value: "道路千萬條"}
// {done: false, value: "安全第一條"}
// {done: false, value: "行車不規範"}
// {done: true, value: undefined}

手寫 async/await

Generator 缺陷:

async 函數對 Generator 函數改進如下:

async/await 做的事情就是將 Generator 函數轉換成 Promise,說白了,async 函數就是 Generator 函數的語法糖,await 命令就是內部 then 命令的語法糖。

const fetchData = (data) => new Promise((resolve) => setTimeout(resolve, 1000, data + 1))

const fetchResult = async function () {
    var result1 = await fetchData(1);
    var result2 = await fetchData(result1);
    var result3 = await fetchData(result2);
    console.log(result3);
}

fetchResult();

可以嘗試通過 Babel[8] 官網轉換一下上述代碼,可以看到其核心就是 _asyncToGenerator 方法。

我們下面來實現它。

function asyncToGenerator(generatorFn) {
    // 將 Generator 函數包裝成了一個新的匿名函數,調用這個匿名函數時返回一個 Promise
    return function() {
        // 生成迭代器,相當於執行 Generator 函數
        // 如上面三碗不過崗例子中的 var canteen = webCanteenGenerator()
        var gen = generatorFn.apply(this, arguments);
        return new Promise(function(resolve, reject) {
            // 利用 Generator 分割代碼片段,每一個 yield 用 Promise 包裹起來
            // 遞歸調用 Generator 函數對應的迭代器,當迭代器執行完成時執行當前的 Promise,失敗時則拒絕 Promise
            function step(key, arg) {
                try {
                    var info = gen[key](arg "key");
                    var value = info.value;
                } catch (error) {
                    reject(error);
                    return;
                }

                if (info.done) {
                    // 遞歸終止條件,完成了就 resolve
                    resolve(value);
                } else {
                    return Promise.resolve(value).then(function(value) {
                        step('next', value);
                    }, function(err) {
                        step('throw', err);
                    });
                }
            }
            return step('next');
        });
    }
}

好了,本文到這裡就告一段落,如果上述代碼你發現有問題的地方,可以在評論區留言,一起探討學習。

相關焦點

  • 如何使用Promise.race() 和 Promise.any() ?
    , 200, 'promise 3 resolved') });  (async () => {     try {         let result = await Promise.race([promise1, promise2, promise3]);         console.log(result);     } catch (err) {         console.error
  • OPPO智美生活全家桶價格已曝光,那發布會還有什麼看點呢?
    今天(10 月 19 日 19:00),OPPO 智美生活發布會要來了,相信這段時間大家都被吊足胃口了,畢竟之前一直傳言的「全家桶」今晚就悉數亮相了。從官方的倒計時預熱海報上我們基本上可以知道這次發布會的「全家桶」雖然不能吃,但是它能聽、能看、能玩還能戴,驚喜可以說接二連三的來。
  • iPadOS 14「隨手寫」功能:解放手指,讓你的 Apple Pencil 更具生產力
    近日,愛範兒參加了蘋果 iPadOS 14「隨手寫」功能的媒體分享會,在這次分享會上,蘋果對 iPadOS 14 中新增的 Apple Pencil「隨手寫」功能進行了介紹。這裡,就不得不說說 iPadOS 14 中新增加的 Apple Pencil 「隨手寫」功能。「隨寫」功能是 iPadOS 14 為 Apple Pencil 打造的一項全新的功能。當你在任意本欄寫,就能及時的將你的寫字轉換為輸入的本,中英文混合也沒問題。
  • JavaScript進階之Ajax的問題和什麼是promise
    jQuery為我們提供Ajax方法url :接口地址 ,後臺給我們提供的接口地址函數:如果接口調用成功會有成功的回調success函數裡面可寫成功調取到接口後的業務邏輯async:默認值: true。默認設置下,所有請求均為異步請求。如果需要發送同步請求,請將此選項設置為 false。注意,同步請求將鎖住瀏覽器,用戶其它操作必須等待請求完成才可以執行。data :發送到伺服器的數據。將自動轉換為請求字符串格式。
  • iOS 14代碼曝光蘋果「全家桶套餐」:即將推出Apple One訂閱服務
    首頁 > 見聞 > 關鍵詞 > 蘋果最新資訊 > 正文 iOS 14代碼曝光蘋果「全家桶套餐」:即將推出Apple One訂閱服務
  • 為什麼肯德基全家桶,國外分量那麼大,國內卻很少?太偷工減料了
    每當周末,一家人可能就會聚在肯德基,點一份全家桶,邊聊天邊吃,非常開心。記得我小時候能夠吃到肯德基全家桶,就是天大的快樂了,那是能回憶起來的最美好的時光了。 那麼今天蘿媽就想和大家聊聊關於,為什麼肯德基全家桶,國外分量那麼大,國內卻很少?太氣人了,真是偷工減料!
  • 抱住這個「巨大的桶」!有吃有玩,還有網紅爆款流心慕斯蛋糕!
    本文轉載自【微信公眾號:瀋陽吃貨王,ID:sychw024】經微信公眾號授權轉載,如需轉載與原文作者聯繫作為一年一度的限定網紅款「肯德基聖誕巨大的桶」將近半人高的桶身抱著都有點費勁果然是史上最最最豪華巨巨巨巨巨巨巨巨巨大超超超超超超有料的桶!!!
  • 就決定是你了寶可夢聯名全家桶即將發售
    雙方今回帶來了聯名 T-Shirt、帆布棒球帽、Tote 袋、iPhone 殼、AirPods 套、玻璃杯和鑰匙圈等單品,設計上以 皮卡丘、夢幻 和 耿鬼 等三位人氣 Pokémon 為主軸,將其身影注入各單品之中,結合「GOTCHA!」字樣及 thisisneverthat 的標誌,可謂是《寶可夢》全家桶。
  • 實驗室擺30桶鎂粉!爆炸前晚,遇難學生曾向環保局舉報「無下文」
    ▲北交大實驗室擺30桶鎂粉,遇難學生爆炸前一天曾打電話舉報。北京交通大學2號實驗樓26日早上發生爆炸起火,校方指稱,學生在進行「垃圾滲濾液汙水處理科研試驗」時發生事故,3名學生不幸遇難。事發前幾天,幾名學生25日曾以周邊居民的名義,向環保部門舉報實驗室中的「30桶鎂粉」,但並無下文,最終還是發生悲劇。當時爆炸現場火勢相當猛烈,團團濃煙遮掩了大片藍天。一名學生表示,他早上在上課時,突然聽到3聲爆炸聲,之後就看到對面的2號樓起火。到了中午12點半左右時,現場已沒有明火,但有工作人員在空地處,將疑似屍體移至白布並蓋住。
  • 微軟Office 三合一 APP 上線,可以告別全家桶了
    ▎主要功能打開應用,第一眼看到的就是極其精簡和乾淨的界面,只有「主頁」「+」和「操作」三個選項,再點擊「+」,又可以看到有備註、拍照和文檔三個選項。其他地方我們後面再討論,先看看要介紹的 App 主要功能——「文檔」:熟悉的三個圖標,Word、Excel、PowerPoint 都有,選項很簡單,而且支持掃描和模板,又能滿足一切額外的需求。
  • 肯德基全家桶遇勁敵了,麥當勞也出了個金拱門桶,49還買一送一
    肯德基全家桶遇勁敵了,麥當勞也出了個金拱門桶,49還買一送一現在人沒有時間回家做飯吃,每天午飯晚飯很多都是吃點快餐填飽肚子。而來自美國的快餐炸雞漢堡就格外的受到大家的喜愛。什麼麥當勞肯德基,賣的東西都差不多,不是炸雞就是漢堡,你家賣冰淇淋我家也賣冰淇淋。
  • 全家便利店零成本「閃電開票」的秘密丨螞蟻開放日福州站分享
    但知名便利店連鎖品牌「全家」卻通過電子發票讓這些問題迎刃而解。 從一張帶二維碼的小票開始,打開支付寶掃一掃,選擇開票抬頭,到保存至「發票管家」,僅僅用了不到10秒的時間。這個過程消費者全程自助,「全家」店員不僅無需任何操作,還可以通過電子發票商家後臺進行便捷的查詢統計。
  • 百度輸入法正式更新,手寫輸入全面升級
    近期,百度輸入法正式更新了全新版本,通過自研的飛槳框架,將手寫輸入的準確率提升到了96%,解決了手寫識別率低的難題。同時,百度基於20年積累的算法經驗,大幅提升了疊寫(很多個字疊在一起寫)和連寫(快速隔開寫)的輸入體驗。