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

觀感度:🌟🌟🌟🌟🌟前端

口味:海底撈git

烹飪時間:15mingithub

本文已收錄在前端食堂同名倉庫Github github.com/Geekhyt,歡迎光臨食堂,若是以爲酒菜還算可口,賞個 Star 對食堂老闆來講是莫大的鼓勵。

手寫 Promise 全家桶

Promise/A+ 規範鎮樓!web

若是你沒讀過 Promise/A+ 規範也不要緊,我幫你總結了以下三部分重點:segmentfault

不過建議看完本文後仍是要親自去讀一讀,很少 bb,開始展現。設計模式

規範重點

1.Promise 狀態

Promise 的三個狀態分別是 pendingfulfilledrejected數組

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

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

2.Then 方法

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

promise.then(onFulfilled, onRejected)
  • 1.then 方法接受兩個函數做爲參數,且參數可選。
  • 2.若是可選參數不爲函數時會被忽略。
  • 3.兩個函數都是異步執行,會放入事件隊列等待下一輪 tick。
  • 4.當調用 onFulfilled 函數時,會將當前 Promise 的 value 值做爲參數傳入。
  • 5.當調用 onRejected 函數時,會將當前 Promise 的 reason 失敗緣由做爲參數傳入。
  • 6.then 函數的返回值爲 Promise。
  • 7.then 能夠被同一個 Promise 屢次調用。

3.Promise 解決過程

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

針對 x 的不一樣值處理如下幾種狀況:

  • 1.x 等於 Promise

拋出 TypeError 錯誤,拒絕 Promise。

  • 2.x 是 Promise 的實例

若是 x 處於待定狀態,那麼 Promise 繼續等待直到 x 兌現或拒絕,不然根據 x 的狀態兌現/拒絕 Promise。

  • 3.x 是對象或函數

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

若是執行報錯,則將以對應的失敗緣由拒絕 Promise。

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

  • 4.若是 x 不是對象或函數

以 x 做爲值執行 Promise。

手寫 Promise

1.首先定義 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() 函數接收兩個回調函數做爲參數,分別是 onFulfilledonRejected

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 的屬性 onFulfilledFnonRejectedFn 中,

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

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

目前支持 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 缺陷:

  • 1.函數外部沒法捕獲異常
  • 2.多個 yield 會致使調試困難

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

  • 1.內置執行器
  • 2.更好的語義
  • 3.更廣的適用性
  • 4.返回值是 Promise

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 官網轉換一下上述代碼,能夠看到其核心就是 _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);
                    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');
        });
    }
}

好了,本文到這裏就告一段落,若是上述代碼你發現有問題的地方,能夠在評論區留言,一塊兒探討學習。

一些參考資源

❤️愛心三連擊

1.看到這裏了就點個贊支持下吧,你的是我創做的動力。

2.關注公衆號前端食堂,你的前端食堂,記得按時吃飯

3.本文已收錄在前端食堂 github.com/Geekhyt,求個小星星,感謝Star。

image

相關文章
相關標籤/搜索