Promise自定義,看我如何征服你

丁同亞的部落格發表於2020-12-06

自定義程式碼

這裡是我自定義的Promise,如果想看原版,可以跳過最後有符合PromiseA+規範的原始碼

class 承諾 {
    constructor(處理器函式) { //1. 處理器函式是在_new 承諾(處理器函式)_的時候當做引數傳入到建構函式裡的,建構函式裡會執行這個處理器函式.
        let self = this //2.註冊函式執行時可能沒有字首,如果註冊函式裡用this可能就是window,這裡把this賦給變數,形成閉包.
        this.狀態 = '等待...' //3.這個屬性有三個值,預設是等待,只有呼叫註冊成功或註冊失敗才會改變這個屬性的值,**只能改變一次**
        this.成功值 = undefined //4.這個成功值是由使用者呼叫註冊成功函式的時候傳進函式裡的,然後我們在註冊成功函式給這個屬性賦值,簡單點說就是形參賦值給屬性
        this.失敗原因 = undefined //5.同上
        //說明:這裡需要了解'那時()'函式,可以看完'那時()'再來看,這兩在非同步的情況才會用到,使用者在呼叫'那時()'的時候,承諾仍然是等待狀態(也就是說註冊成功和註冊失敗兩個函式沒有被呼叫)
        this.成功回撥函式組 = [] //6.承諾變為成功時要呼叫的函式,多次呼叫then傳函式,我們就把'那時()'傳進來的函式push到這個陣列裡,
        this.失敗回撥函式組 = [] //7.同上,承諾變為失敗時要呼叫的函式

        let 註冊成功 = function (成功值) { //7.'註冊成功()'函式當實參傳進了處理器函式,這樣使用者在編寫處理器函式得時候可以根據情況呼叫.
            if (self.狀態 === '等待...') { //8.這個判斷的作用:承諾的狀態只能由等待->成功或等待->失敗,而且只能改變一次.我們預設初始態是等待,如果不是等待說明之前已經呼叫過'註冊成功或註冊失敗'
                self.成功值 = 成功值 //9.將使用者呼叫'註冊成功(成功值)'函式傳進來的值由物件屬性儲存,這個值在呼叫'那時()'傳進來的函式時會用到
                self.狀態 = '成功' //10.呼叫註冊成功,改變承諾狀態為成功
                for (let 函式 of self.成功回撥函式組) { //11.呼叫'那時()'傳進來的函式
                    函式(成功值)
                }
                // this.成功回撥函式組.forEach(函式=>函式())
            }

        }
        let 註冊失敗 = function (失敗原因) { //12. 同上
            if (self.狀態 === '等待...') {
                self.失敗原因 = 失敗原因
                self.狀態 = '失敗'
                for (let 函式 of self.失敗回撥函式組) {
                    函式(失敗原因)
                }
                // this.成功回撥函式組.forEach(函式=>函式())
            }

        }


        try {
            處理器函式(註冊成功, 註冊失敗) //13.執行處理器函式,把我們定義好的兩個函式傳進去,這樣使用者就可以用這兩個函式來改變承諾的狀態和值
        } catch (錯誤) {
            註冊失敗(錯誤) //14.處理器函式是使用者編寫的,有可能報錯,出錯了我們就呼叫註冊失敗()來改變這個承諾的狀態和值
        }
    }

    那時(成功的回撥, 失敗的回撥) { //15.物件的'那時()'方法,由使用者傳進來兩個函式引數,規定當前承諾物件為成功時呼叫第一個,失敗呼叫第二個
        let self = this
        成功的回撥 = typeof 成功的回撥 === 'function' ? 成功的回撥 : 成功值 => 成功值
        失敗的回撥 = typeof 失敗的回撥 === 'function' ? 失敗的回撥 : 失敗原因 => {
            throw 失敗原因
        } //16.這兩個引數是可選引數,當他們不是函式時我們給他預設值

        let 新的承諾 = new 承諾((註冊成功, 註冊失敗) => { //17.返回一個新的承諾,這樣就可以鏈式呼叫'那時()'了,這裡要注意新承諾的狀態和值由'那時()'傳進來的函式執行情況決定
            //18.判斷承諾的狀態,決定立即執行回撥還是將回撥函式push到回撥函式組裡等使用者呼叫註冊成功()在註冊成功()裡執行
            if (self.狀態 === '等待...') { //19.如果是等待,顯然承諾的成功值或失敗值還是undefined,所以我們把他push到回撥函式組裡,讓他在註冊成功()或註冊失敗()函式裡呼叫
                self.成功回撥函式組.push(() => { //20.這個函式要非同步,因為我們會在裡面用到新承諾,然而新承諾現在還沒被賦值,要徹底理解這裡應該需要js執行機制知識,目前我還沒有....
                    setTimeout(() => {
                        try {
                            let 回撥的返回值 = 成功的回撥(self.成功值) //21.呼叫使用者傳進來的函式得到返回值,如果使用者沒有寫返回語句預設是返回undefined
                            善後處理(新的承諾, 回撥的返回值, 註冊成功, 註冊失敗) //22.根據返回值決定我們這個新承諾的值和狀態 這裡傳進去的是我們新承諾的註冊成功和註冊失敗函式,我們在裡面呼叫他們來改變我們新承諾的狀態
                        } catch (錯誤) {
                            註冊失敗(錯誤) //23.如果執行使用者的函式出錯就把我們新承諾的狀態和值改變
                        }

                    })
                })

                self.失敗回撥函式組.push(() => { //24.同上
                    setTimeout(() => {
                        try {
                            let 回撥的返回值 = 失敗的回撥(self.失敗原因)
                            善後處理(新的承諾, 回撥的返回值, 註冊成功, 註冊失敗)
                        } catch (錯誤) {
                            註冊失敗(錯誤)
                        }
                    })
                })

            }
            if (self.狀態 === '成功') { //25.如果承諾的狀態是成功的說明註冊成功()函式已經被呼叫了,承諾的狀態和值都被使用者改變了,
                //我們可以取到對應的值來傳進回撥裡,讓使用者用.
                setTimeout(() => {
                    try { //邏輯同20-23
                        let 回撥的返回值 = 成功的回撥(self.成功值)
                        善後處理(新的承諾, 回撥的返回值, 註冊成功, 註冊失敗)
                    } catch (錯誤) {
                        註冊失敗(錯誤)
                    }

                })
            }
            if (self.狀態 === '失敗') { //同上
                setTimeout(() => {
                    try {
                        let 回撥的返回值 = 失敗的回撥(self.失敗原因)
                        善後處理(新的承諾, 回撥的返回值, 註冊成功, 註冊失敗)
                    } catch (錯誤) {
                        註冊失敗(錯誤)
                    }
                })
            }
        })


        return 新的承諾

    }

}

工具函式,這個函式才是真核心


//26.這個是核心,涉及到遞迴,這裡我寫的和PromiseA+規範的實現不同,他判斷的是thenable,相容性好,我只是為了理解Promise原理所以就簡化了.

function 善後處理(新的承諾, 回撥的返回值, 註冊成功, 註冊失敗) {

    // let p2 = p.那時((成功值)=>{
    //     return p2
    // })

    if (回撥的返回值 instanceof 承諾) { //27.判斷使用者寫的回撥函式的返回值是不是一個承諾,如果不是承諾就直接呼叫我們新承諾的註冊成功()函式改變我們新承諾的狀態和值
        //如果返回值是一個承諾我們就得到這個承諾的值,把這個值給我們新承諾的註冊函式
        if (新的承諾 === 回撥的返回值) {
            //28.這裡是為了解決這種使用情況
            // let p2 = p.那時((成功值)=>{
            //     return p2
            // })
            註冊失敗(new TypeError('迴圈引用'))
            return
        }
        try {
            回撥的返回值.那時((成功值) => {
                善後處理(新的承諾, 成功值, 註冊成功, 註冊失敗) //29.如果使用者返回的承諾的值還是一個承諾,繼續'那時()'直到不是承諾
                //注意:這裡傳進去的註冊成功,註冊失敗是我們新承諾的註冊函式,遞迴進去,當不是承諾時就改變我們新承諾的狀態和值了,然後遞迴一層層返回
                //這裡是進遞迴,其他情況就是出遞迴
            }, (失敗原因) => {
                註冊失敗(失敗原因)
            })
        } catch (錯誤) {
            註冊失敗(錯誤)
        }
    } else {
        註冊成功(回撥的返回值)
    }


}

自定義完成,我們拉出來遛一遛

//測試一

p = new 承諾((註冊成功, 註冊失敗) => {
    setTimeout(()=>{
        註冊失敗('abc')
    },1000)

})

p.那時((成功的值) => {
    console.log(成功的值)

},(失敗原因)=>{
    console.log(失敗原因)
})
//輸出abc
//測試二:返回值是承諾

p = new 承諾((註冊成功, 註冊失敗) => {
    setTimeout(() => {
        註冊成功('我是最外面的')
    }, 1000)

})

p.那時((成功值) => {
        console.log(成功值)
        let p11 = new 承諾((註冊成功, 註冊失敗) => {
            let p22 = new 承諾((註冊成功, 註冊失敗) => {
                註冊成功('最裡層')
            })
            註冊成功(p22)
        })
        return p11
    }, 1)
    .那時((成功值) => {
        console.log(成功值)
    }, (失敗原因) => {
        console.log(失敗原因)
    })
//輸出:
//我是最外層
//我是最裡層
//測試三:失敗穿透
p = new 承諾((註冊成功, 註冊失敗) => {
    throw '(╥╯^╰╥)'
    setTimeout(() => {
        註冊成功('♪(´▽`)')

    }, 1000)

})

p.那時((成功的值) => {
    console.log(成功的值)
    
},(失敗原因)=>{
    console.log('第一次失敗:'+失敗原因)
    throw '(╥╯^╰╥))'
}).那時(
    (成功的值) => {
        console.log(成功的值)

    }
).那時(null,(失敗原因) => {
    console.log('失敗穿透'+失敗原因)
})
//輸出
//第一次失敗:(╥╯^╰╥)
//失敗穿透(╥╯^╰╥))  

到這裡就結束了,下面是通過PromiseA+測試的原始碼.

function Promise(executor) {
    let self = this;
    self.value = undefined; // 成功的值
    self.reason = undefined; // 失敗的值
    self.status = 'pending'; // 目前promise的狀態pending
    self.onResolvedCallbacks = []; // 可能new Promise的時候會存在非同步操作,把成功和失敗的回撥儲存起來
    self.onRejectedCallbacks = [];

    function resolve(value) { // 把狀態更改為成功
        if (self.status === 'pending') { // 只有在pending的狀態才能轉為成功態
            self.value = value;
            self.status = 'resolved';
            self.onResolvedCallbacks.forEach(fn => fn()); // 把new Promise時非同步操作,存在的成功回撥儲存起來
        }
    }

    function reject(reason) { // 把狀態更改為失敗
        if (self.status === 'pending') { // 只有在pending的狀態才能轉為失敗態
            self.reason = reason;
            self.status = 'rejected';
            self.onRejectedCallbacks.forEach(fn => fn()); // 把new Promise時非同步操作,存在的失敗回撥儲存起來
        }
    }
    try {
        // 在new Promise的時候,立即執行的函式,稱為執行器
        executor(resolve, reject);
    } catch (e) { // 如果執行executor丟擲錯誤,則會走失敗reject
        reject(e);
    }
}
// then呼叫的時候,都是屬於非同步,是一個微任務
// 微任務會比巨集任務先執行
// onFulfilled為成功的回撥,onRejected為失敗的回撥
Promise.prototype.then = function (onFulfilled, onRejected) {
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : val => val;
    onRejected = typeof onRejected === 'function' ? onRejected : err => {
        throw err
    }
    let self = this;
    let promise2;
    // 上面講了,promise和jquery的區別,promise不能單純返回自身,
    // 而是每次都是返回一個新的promise,才可以實現鏈式呼叫,
    // 因為同一個promise的pending resolve reject只能更改一次
    promise2 = new Promise((resolve, reject) => {
        if (this.status === 'resolved') {
            // 為什麼要加setTimeout?
            // 首先是promiseA+規範要求的
            // 其次是大家寫的程式碼,有的是同步,有的是非同步
            // 所以為了更加統一,就使用為setTimeout變為非同步了,保持一致性
            setTimeout(() => {
                try { // 上面executor雖然使用try catch捕捉錯誤
                    // 但是在非同步中,不一定能夠捕捉,所以在這裡
                    // 用try catch捕捉
                    let x = onFulfilled(self.value);
                    // 在then中,返回值可能是一個promise,所以
                    // 需要resolvePromise對返回值進行判斷
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            }, 0)
        }
        if (self.status === 'rejected') {
            setTimeout(() => {
                try {
                    let x = onRejected(self.reason);
                    resolvePromise(promise2, x, resolve, reject);
                } catch (e) {
                    reject(e);
                }
            }, 0)
        }
        if (self.status === 'pending') {
            self.onResolvedCallbacks.push(() => {
                setTimeout(() => {
                    try {
                        let x = onFulfilled(self.value);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0)
            });
            self.onRejectedCallbacks.push(() => {
                setTimeout(() => {
                    try {
                        let x = onRejected(self.reason);
                        resolvePromise(promise2, x, resolve, reject);
                    } catch (e) {
                        reject(e);
                    }
                }, 0)
            });
        }
    });
    return promise2
}


function resolvePromise(promise2, x, resolve, reject) {
    // 3.從2中我們可以得出,自己不能等於自己
    // 當promise2和x是同一個物件的時候,則走reject
    if (promise2 === x) {
        return reject(new TypeError('Chaining cycle detected for promise'))
    }
    // 4.因為then中的返回值可以為promise,當x為物件或者函式,才有可能返回的是promise
    let called
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        // 8.從第7步,可以看出為什麼會存在丟擲異常的可能,所以使用try catch處理
        try {
            // 6.因為當x為promise的話,是存在then方法的
            // 但是我們取一個物件上的屬性,也有可能出現異常,我們可以看一下第7步
            let then = x.then

            // 9.我們為什麼在這裡用call呢?解決了什麼問題呢?可以看上面的第10步
            // x可能還是個promise,那麼就讓這個promise執行
            // 但是還是存在一個惡作劇的情況,就是{then:{}}
            // 此時需要新增一個判斷then是否函式
            if (typeof then === 'function') {
                then.call(x, (y) => { // y是返回promise後的成功結果
                    // 一開始我們在這裡寫的是resolve(y),但是考慮到一點
                    // 這個y,有可能還是一個promise,
                    // 也就是說resolve(new Promise(...))
                    // 所以涉及到遞迴,我們把resolve(y)改成以下

                    // 12.限制既調resolve,也調reject
                    if (called) return
                    called = true

                    resolvePromise(promise2, y, resolve, reject)
                    // 這樣的話,程式碼會一直遞迴,取到最後一層promise

                    // 11.這裡有一種情況,就是不能既調成功也調失敗,只能挑一次,
                    // 但是我們前面不是處理過這個情況了嗎?
                    // 理論上是這樣的,但是我們前面也說了,resolvePromise這個函式
                    // 是所有promise通用的,也可以是別人寫的promise,如果別人
                    // 的promise可能既會調resolve也會調reject,那麼就會出問題了,所以我們接下來要
                    // 做一下限制,這個我們寫在第12步

                }, (err) => { // err是返回promise後的失敗結果
                    if (called) return
                    called = true
                    reject(err)
                })
            } else {
                if (called) return;
                called = true;
                resolve(x) // 如果then不是函式的話,那麼則是普通物件,直接走resolve成功
            }
        } catch (e) { // 當出現異常則直接走reject失敗
            if (called) return
            called = true
            reject(e)
        }
    } else { // 5.x為一個常量,則是走resolve成功
        resolve(x)
    }
}







module.exports = Promise;

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}
//執行命令promises-aplus-tests promise.js檢測是否符合promiseA+規範,先安裝包

相關文章