手寫Promise:從易到難,一步一步補完Promise,講解每一行代碼的做用

前言

手寫Promise一直是前端童鞋很是頭疼的問題,也是面試的高頻題。網上有不少手寫Promise的博客,但大部分都存在或多或少的問題。
下面咱們根據A+規範,手寫一個Promise前端

基礎結構

在此部分,先把Promise的基礎結構寫出來。
直接上代碼node

// 規範2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三個狀態:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();

// 根據規範2.2.1到2.2.3
class _Promise {
    constructor(executor) {
        // 默認狀態爲 PENDING
        this.status = PENDING;
        // 存放成功狀態的值,默認爲 undefined
        this.value = undefined;
        // 存放失敗狀態的值,默認爲 undefined
        this.reason = undefined;

        // 成功時,調用此方法
        let resolve = (value) => {
            // 狀態爲 PENDING 時才能夠更新狀態,防止 executor 中調用了兩次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
            }
        };

        // 失敗時,調用此方法
        let reject = (reason) => {
            // 狀態爲 PENDING 時才能夠更新狀態,防止 executor 中調用了兩次 resovle/reject 方法
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
            }
        };

        try {
            // 當即執行,將 resolve 和 reject 函數傳給使用者
            executor(resolve, reject);
        } catch (error) {
            // 發生異常時執行失敗邏輯
            reject(error);
        }
    }

    // 包含一個 then 方法,並接收兩個參數 onFulfilled、onRejected
    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value);
        }

        if (this.status === REJECTED) {
            onRejected(this.reason);
        }
    }
}

export default _Promise;

接下來咱們用測試代碼測一下面試

const promise = new _Promise((resolve, reject) => {
        resolve('成功');
        setTimeout(() => {
            console.log('settimeout1');
        }, 0);
    })
        .then(
            (data) => {
                console.log('success', data);
                setTimeout(() => {
                    console.log('settimeout2');
                }, 0);
            },
            (err) => {
                console.log('faild', err);
            }
        )
        .then((data) => {
            console.log('success2', data);
        });

控制檯打印
image.png
能夠看到,在executor方法中的異步行爲在最後才執行
並且若是把resolve方法放到setTimeout中,會沒法執行
這固然是不妥的。
接下來咱們優化一下異步數組

executor方法中的異步

在上一小節中,咱們將resolve的結果值存放到了this.value裏。
優化後的代碼以下:promise

// 規範2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三個狀態:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();

class _Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        // 存放成功的回調
        this.onResolvedCallbacks = [];
        // 存放失敗的回調
        this.onRejectedCallbacks = [];
        // 這裏使用數組,是由於若是屢次調用then,會把方法都放到數組中。
        // 可是目前這個版本還不支持then的鏈式調用

        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                // 依次將對應的函數執行
                // 在此版本中,這個數組實際上長度最多隻爲1
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        }

        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                // 依次將對應的函數執行
                // 在此版本中,這個數組實際上長度最多隻爲1
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        }

        try {
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }

    then(onFulfilled, onRejected) {
        if (this.status === FULFILLED) {
            onFulfilled(this.value)
        }

        if (this.status === REJECTED) {
            onRejected(this.reason)
        }
        // 上面兩個分支是:支持resolve函數執行的時候,若是不在異步行爲裏執行resolve的話,會當即執行onFulfilled方法

        if (this.status === PENDING) {
            // 若是promise的狀態是 pending,須要將 onFulfilled 和 onRejected 函數存放起來,等待狀態肯定後,再依次將對應的函數執行
            this.onResolvedCallbacks.push(() => {
                onFulfilled(this.value)
            });

            // 若是promise的狀態是 pending,須要將 onFulfilled 和 onRejected 函數存放起來,等待狀態肯定後,再依次將對應的函數執行
            this.onRejectedCallbacks.push(() => {
                onRejected(this.reason);
            })
        }
    }
}

咱們用測試方法測一下:瀏覽器

const promise = new _Promise((resolve, reject) => {
            setTimeout(() => {
                console.log('settimeout1');
                resolve('成功');
            }, 0);
        })
        .then(
            (data) => {
                console.log('success', data);
                setTimeout(() => {
                    console.log('settimeout2');
                }, 0);
                return data;
            },
            (err) => {
                console.log('faild', err);
            }
        )
        .then((data) => {
            console.log('success2', data);
        });

控制檯結果:
image.png
能夠看到,異步順序是正確的,先執行settimeout1,再執行success
可是不支持鏈式的then調用,也不支持在then中返回一個新的Promisedom

支持鏈式調用的Promise

接下來咱們將完整實現一個支持鏈式調用的Promis異步

// 規範2.1:A promise must be in one of three states: pending, fulfilled, or rejected.
// 三個狀態:PENDING、FULFILLED、REJECTED
const PENDING = Symbol();
const FULFILLED = Symbol();
const REJECTED = Symbol();

class _Promise {
    constructor(executor) {
        this.status = PENDING;
        this.value = undefined;
        this.reason = undefined;
        // 存放成功的回調
        this.onResolvedCallbacks = [];
        // 存放失敗的回調
        this.onRejectedCallbacks = [];
        // 這裏使用數組,是由於若是屢次調用then,會把方法都放到數組中。
        // 可是目前這個版本還不支持then的鏈式調用

        let resolve = (value) => {
            if (this.status === PENDING) {
                this.status = FULFILLED;
                this.value = value;
                // 依次將對應的函數執行
                // 在此版本中,這個數組實際上長度最多隻爲1
                this.onResolvedCallbacks.forEach(fn => fn());
            }
        }

        let reject = (reason) => {
            if (this.status === PENDING) {
                this.status = REJECTED;
                this.reason = reason;
                // 依次將對應的函數執行
                // 在此版本中,這個數組實際上長度最多隻爲1
                this.onRejectedCallbacks.forEach(fn => fn());
            }
        }

        try {
            // 當即執行executor方法
            executor(resolve, reject)
        } catch (error) {
            reject(error)
        }
    }
    
    // 這裏就是最關鍵的then方法
    then(onFulfilled, onRejected) {
        // 克隆this,由於以後的this就不是原promise的this了
        const self = this;
        
        // 判斷兩個傳入的方法是否是funcion,若是不是,那麼給一個function的初始值
        onnFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
        onRejected = typeof onRejected === 'function'?onRejected:reason => { throw new Error(reason instanceof Error ? reason.message:reason) }
        
        // 返回一個新的promise,剩下的邏輯都在這個新的promise裏進行
        return new _Promise((resolve, reject) => {
            if (this.status === PENDING) {
                // 若是promise的狀態是 pending,須要將 onFulfilled 和 onRejected 函數存放起來,等待狀態肯定後,再依次將對應的函數執行
                self.onResolvedCallbacks.push(() => {
                    // 使用settimeout模擬微任務
                    setTimeout((0 => {
                        // self.value是以前存在value裏的值
                        const result = onFulfilled(self.value);
                        // 這裏要考慮兩種狀況,若是onFulfilled返回的是Promise,則執行then
                        // 若是返回的是一個值,那麼直接把值交給resolve就行
                        result instanceof _Promise ? result.then(resolve, reject) : resolve(result);
                    }, 0)
                    onFulfilled(self.value)
                });

                // 若是promise的狀態是 pending,須要將 onFulfilled 和 onRejected 函數存放起來,等待狀態肯定後,再依次將對應的函數執行
                
                
                
                // reject也要進行同樣的事
                self.onRejectedCallbacks.push(() => {
                    setTimeout(() => {
                        const result = onRejected(self.reason);
                        // 不一樣點:此時是reject
                        result instanceof _Promise ? result.then(resolve, reject) : reject(result);
                    }, 0)
                })
            }
            
            // 若是不是PENDING狀態,也須要判斷是否是promise的返回值
            if (self.status === FULFILLED) {
                setTimeout(() => {
                    const result = onFulfilled(self.value);
                    result instanceof _Promise ? result.then(resolve, reject) : resolve(result);
                });
            }

            if (self.status === REJECTED) {
                setTimeout(() => {
                    const result = onRejected(self.reason);
                    result instanceof _Promise ? result.then(resolve, reject) : reject(result);
                })
            }
        
        })
        // 到這裏,最難的then方法已經寫完了
    }
}

額外補充

catch、靜態resolve、靜態reject方法

catch方法的通常用法是
new _Promise(() => {...}).then(() => {...}).catch(e => {...})
因此它是一個和then同級的方法,它實現起來很是簡單:函數

class _Promise{
    ...

    catch(onRejected) {
        return this.then(null, onRejected);
    }
}

靜態resolve、靜態reject的用法:
_Promise.resolve(() => {})
這樣能夠直接返回一個_Promise
這塊的實現,參考then中返回_Promise的那一段,就能實現
reject相似測試

class _Promise{
    ...
    
    static resolve(value) {
        if (value instanceof Promise) {
            // 若是是Promise實例,直接返回
            return value;
        } else {
            // 若是不是Promise實例,返回一個新的Promise對象,狀態爲FULFILLED
            return new Promise((resolve, reject) => resolve(value));
        }
    }
    static reject(reason) {
        return new Promise((resolve, reject) => {
            reject(reason);
        })
    }
}

優化setTimeout變成微任務

最後再說一個關於微任務的
setTimeout畢竟是個宏任務,咱們能夠用MutationObserver來模擬一個微任務,只要將下面的nextTick方法替換setTimeout方法便可

function nextTick(fn) {
  if (process !== undefined && typeof process.nextTick === "function") {
    return process.nextTick(fn);
  } else {
    // 實現瀏覽器上的nextTick
    var counter = 1;
    var observer = new MutationObserver(fn);
    var textNode = document.createTextNode(String(counter));

    observer.observe(textNode, {
      characterData: true,
    });
    counter += 1;
    textNode.data = String(counter);
  }
}

這個方法的原理不難看懂,就是在dom裏建立了一個textNode,用MutationObserver監控這個node的變化。在執行nextTick方法的時候手動修改這個textNode,觸發MutationObserver的callback,這個callback就會在微任務隊列中執行。
注意MutationObserver的兼容性

總結

我我的感受完整理解Promise的源碼仍是比較考驗代碼功底的,一開始建議把源碼放在編譯器裏一點一點調試着看,若是實在不知道怎麼下手,也能夠把代碼背下來,慢慢咀嚼。實際上,背下來以後,人腦對這個東西會有一個緩慢的理解過程,到了某一天會感受恍然大悟。

參考文章

https://zhuanlan.zhihu.com/p/183801144

Promise/A+規範

相關文章
相關標籤/搜索