手寫Promise看著一篇就足夠了

j植樹發表於2020-09-01

概要

本文主要介紹了Promise上定義的api的特性,以及手寫如何實現這些特性。目的是把學習過程中的產出以部落格的方式輸出,鞏固知識,也便於之後複習

部落格思路

mdn上搜尋Promise,瞭解類和api的定義:

  • 定義了哪些屬性,分別代表什麼含義
  • api需要傳什麼引數,返回什麼值,可能丟擲什麼異常
  • 看官方給出的用例,猜想內部可能的實現
  • 編寫原始碼,用官方用例驗證檢視返回值是否一致

API的特性與手寫原始碼

建構函式

  • promise有狀態pending、rejectedresolved,所以應該有個變數來儲存狀態
  • 建構函式引數excutor是個同步執行的回撥函式,函式執行的引數是兩個函式resolved和rejected,所以promise內部需要定義兩個函式,並且在構造行數中執行excutor的地方傳入
  • .then中會傳入回撥函式onResolved和onRejected,在resolved和rejected內會分別會觸發對應的回撥函式,所以需要兩個陣列儲存then中傳進來的回撥
  • resolved和rejected只能執行一次,執行後promise的狀態會改變,且引數會傳遞給回撥函式
  • onRejected和onResolved非同步執行
  • excutor執行拋異常會直接執行rejected,所以excutor的執行需要catch錯誤

const PENDING = "PENDING";
const RESOLVED = "resolved";
const REJECTED = "rejected";

function MyPromise(excutor){

    // promise內部儲存著狀態值
    this.status = PENDING;
    this.data = null;
    // then方法傳進來的回撥函式此儲存
    this.onResolvedList = [];
    this.onRejectedList = [];

    let resolved = (value) => {
        // resolved函式只能執行一次,所以先判斷狀態是不是pending
        if(this.status !== PENDING){
            return;
        }
        // 變更狀態為resolved
        this.status = RESOLVED;
        // 資料為傳進來的值
        this.data = value;

        // 判斷是否已經有onResolved回撥已經穿入,有則非同步執行
        if(this.onResolvedList.length > 0){
            setTimeout(() => {
                this.onResolvedList.forEach(onResolved => {
                    onResolved(value);
                });
            }, 0);
        }
    }

    let rejected = (reason) => {
        if(this.status !== PENDING){
            return
        }

        this.status = REJECTED;
        this.data = reason;

        if(this.onRejectedList.length > 0){
            setTimeout(() => {
                this.onRejectedList.forEach(onRejected => {
                    onRejected(reason);
                });
            });
        }
    }

    try{
        // 執行器函式同步執行,且引數為promise內定義的resolve和rejected
        excutor(resolved, rejected);
    }catch(error){
        // 如果執行器函式出錯直接執行rejected
        this.rejected(error);
    }
}

then

  • then會接受兩個回撥函式onResolved和onRejected
  • onResolved和onRejected會非同步呼叫
  • then會返回一個新的promise物件
  • then的引數如果沒傳,那麼value和reason繼續向下傳遞
  • 如果執行then的時候,promise的狀態還是pending,那麼只儲存回撥,並且確保回撥執行後能修改新的promise的狀態
  • 如果觸發的對應的回撥函式執行拋異常,那麼返回的新的回撥函式狀態為rejected,reason則會catch到的error
  • 如果觸發的對應回撥函式執行返回值不是promise物件,那麼返回新的promise狀態為resolved,value則為傳入then的回撥的返回值
  • 如果觸發的對應回撥返回值是promise物件,那麼新的promise返回值的狀態取決於改回撥返回的promise
MyPromise.prototype.then = function(onResolved, onRejected){
    
    // 如果沒有傳onResolved,則設定onResolved為返回value的函式
    onResolved = typeof onResolved === "function" ? onResolved : value => value
    // 如果沒有傳onRejected,則設定onRejected為拋處reason的函式
    onRejected = typeof onRejected === "function" ? onRejected : reason => {throw reason}

    return new MyPromise((resolved, rejected) => {

        // 傳入要執行的回撥函式
        let callBackExcu = (callback) => {
            try{
                let result = callback(this.data);
                if(result instanceof MyPromise){
                    // 如果回撥返回值還是promise則then返回的promise的狀態取決於回撥的返回的promise,成功就執行resolved失敗就執行rejected
                    result.then(resolved, rejected);
                }else{
                    // 如果回撥的返回值不為promise則新的promise狀態為resolved
                    resolved(result)
                }
            }catch(error){
                // 如果回撥執行拋處異常,則新的promise狀態為rejected
                rejected(error);
            }
        }

        if(this.status === PENDING){
            // 如果狀態為pending則儲存回撥且確保回撥執行後能修改當前返回的promise的狀態
            this.onResolvedList.push((value) => {
                callBackExcu(onResolved)
            });
            this.onRejectedList.push((reason) => {
                callBackExcu(onRejected)
            });
        }else{
            // 如果狀態不為pending則根據狀態執行對應的回撥,且修改當前promise的狀態
            switch(this.status){
                case REJECTED:
                    // onRejected非同步執行
                    setTimeout(() => {
                       callBackExcu(onRejected); 
                    });
                    break;
                case RESOLVED:
                    // onResolved非同步執行
                    setTimeout(() => {
                       callBackExcu(onResolved); 
                    });
                    break;
            }
        }
    });
}

catch

catch和then其實差不多,不同點在於傳入的引數只有onRejected,所以

MyPromise.prototype.catch = function(onRejected){
    // catch與then的不同點在於傳入的引數不一樣,不需要傳onResolve
    return this.then(null, onRejected);
}

Promise.resolved

  • resolved會返回一個promise物件
  • 如果傳入的引數本就是一個primise物件則直接返回
  • 如果是一個包含“then”方法的物件,返回新的promise物件,且狀態取決於then函式的執行,如果then的執行中拋錯,則新的promise狀態為rejected
  • then的引數為兩個回撥函式resolved和rejected
  • 如果傳入引數value既不是promise的例項,也不是具備then函式的物件,則返回一個新的promise物件且改物件data就為value
MyPromise.resolve = function(value){
    if(value instanceof MyPromise){
        //  如果傳入的引數本就是一個primise物件則直接返回
        return value;
    }else if(typeof value.then === "function"){
        return new MyPromise((resolved, rejected) => {
            try{
                // then的引數為兩個回撥函式resolved和rejected
                value.then(resolved, rejected);
            }catch(error){
                // 如果then的執行中拋錯,則新的promise狀態為rejected
                rejected(error);
            }
        });
    }else{
        // 如果傳入引數value既不是promise的例項
        return new MyPromise((resolved, rejected) => {
            resolved(value);
        });
    }
}

Promise.rejected

  • 接受引數reason,返回一個狀態為rejected的data為reason的promise例項
MyPromise.reject = function(reason){
    return new MyPromise((resolved, rejected) => {
        rejected(reason);
    });
}

Promise.all

  • 接收的引數是需要滿足可迭代協議,否則會拋錯
  • 返回值是個promise
  • 如果傳入的引數是個空的可迭代的物件,則返回一個狀態為resolved的可promise例項,data為空陣列,
Promise.all([]) // Promise {<resolved>: Array(0)}
Promise.all("") // Promise {<resolved>: Array(0)}
  • 如果傳入的引數中沒有promise例項,或者所有的promise已經是resolved狀態,則返回一個promise狀態為pending,且非同步更新為resolved
let p = Promise.all([1,2,3,4,Promise.resolve(5)])
console.log(p); // Promise {<pending>}
  • 如果存在promise且狀態還是pending,返回一個promise例項,且等所有promise都resolved後,狀態更新為resolved,data為傳入的順序

接下來看下原始碼

// 先定義一個驗證引數是否滿足可迭代協議的方法
const isIterable = function(object){
        return typeof object[Symbol.iterator] === "function"
        && typeof object[Symbol.iterator]() === "object"
        && typeof object[Symbol.iterator]().next === "function"
}

MyPromise.all = function(iterable){
    if(!isIterable(iterable)){
        // 不滿足可迭代協議拋錯
        throw new TypeError("Object is not iterable");
    }

    let data = [];
    let count = 0;
    // 迭代引數生成陣列
    let params = Array.from(iterable);

    return new MyPromise((resolved, rejected) => {
        if(params.length === 0){
            // 如果是空的可迭代物件,返回空陣列
            resolved(data);
        }else{
            params.forEach((element, index) => {
                // 遍歷每個引數,統一處理成promise的例項
                // 這樣就少了一個邏輯分支
                let itemPromise = MyPromise.resolve(element);
                itemPromise.then(
                    value => {
                        // data中的結果需要和傳入引數的順序一致
                        data[index] = value;
                        if(count === params.length - 1){
                            // 說明全都resolved了
                            resolved(data);
                        }
                        count++;
                    },
                    reason => {
                        // reject直接返回
                        rejected(reason);
                    }
                );
            });
        }
    });
}

Promise.race

  • 接收一個可迭代物件,這點與方法"all"相同
  • 返回一個新的promise
  • 返回的promise狀態為pending,非同步更新為resolved
let p = Promise.race([1,2,3,4]);
console.log(p); // Promise {<pending>}

p.then(
    value => {
        console.log(value); // Promise{<resolved>: 1}
    }
);
  • 傳入的若干promise中,只要有一個promise最先resolved或者rejected,則返回的promise狀態更新為resolved
let p1 = new Promise((resolved, rejected) => {
    setTimeout(() => {
        resolved("p1");
    }, 10);
});

let p2 = new Promise((resolved, rejected) => {
    setTimeout(() => {
        resolved("p2");
    }, 100);
});

let p = Promise.race([p2, p1])

p.then(
    value => {
        console.log(value); // p1
    }
);

最後來看一下自己原始碼的實現

MyPromise.race = function(iterable){
    if(!isIterable(iterable)){
        // 不滿足可迭代協議拋錯
        throw new TypeError("Object is not iterable");
    }

    const params = Array.from(iterable);

    return new MyPromise((resolved, rejected) => {
        params.forEach((element, index) => {
            const itemPromise = MyPromise.resolve(element);

            itemPromise.then(
                value => {
                    // 只要有一個promise resolved直接返回
                    resolved(value);
                },
                error => {
                    // 只要有一個promise rejected直接返回
                    rejected(error);
                }
            );
        });
    });
}

相關文章