前端面試之手寫Promise

前言

最近總是聽到前端面試要求手寫promise,promise作爲解決前端回調地獄的方案被廣泛運用在前端異步調用上,nodejs更是出了一版返回promise IO接口,所以無論從面試還是實際工作中熟悉promise都是一個合格前端必備技能。

分析Promise

我們知道要實現一個功能,首先要了解需求是什麼,實現Promise也是如此。

那麼Promise到底提供了哪些功能?我們先把Promise中常用的方法列出來

// 使用Promise的標準開頭 new Promise(),可見Promise是一個類

class Promise {
    /**
     * 常用方式Promise.resolve(something).then(...)
     * 可見Promise.resolve返回一個新的Promise實例
     **/
    static resolve() {}
    
    // 同上
    static reject() {}
    
    /**
     * 構造函數接收一個函數作爲入參
     * 該函數會接收resolve,reject作爲入參,並且立即執行
     * Promise將在fn調用resolve/reject後決議
     */
    constructor(fn) {}
    
    /**
     * 根據constructor我們可以推斷Promise有三個狀態分別是
     * pending,fullfilled,rejected
     * 我們需要一個內部變量來保存promise的當前狀態
     */
    _status = 'pending' // promise的初始狀態爲pending
    
    /**
     * 我們需要一個變量保存promise的決議結果
     * 考慮 new Promise((resolve) => resolve(2));
     **/
     _result
    
    /**
     * 考慮一下resolve做了什麼事情
     * 其實resolve改變promise的狀態並將入參作爲回調函數的入參
     **/
    resolve(ret) {}
    
    // 同上
    reject(ret) {}
    
    /**
     * then稱得上是promise的核心方法,到底then做了什麼,我們考慮一下
     * then會接收兩個函數,在promise的狀態發生改變後會調用對應的函數
     * 所以在這裏then的作用應當是個事件註冊器。
     * 需要注意的是then是能多次調用的
     * const promise = new Promise(fn)
     * promise.then(fn1)
     * promise.then(fn2)
     * 另外then是支持鏈式調用的,如promise.then(fn3).then(fn4)
     * 所以調用then還應當返回一個promise對象
     **/
    then(fn, fn2) {}
    
    // 描述完then後,我們發現我們需要兩個回調隊列來保存使用then註冊的回調
    _successCallback = []
    _errorCallback = []
    
    // 我們再定義一個內部方法來異步執行這些回調函數
    // 使用入參type區分執行successCallback還是errorCallback
    _runCallbackAsync(type) {}
}

實現

分析完Promise的核心功能後,讓我們依次開始實現這些接口。

// 先不看靜態方法和實例方法,從構造器開始實現
class Promise {
    ...
    constructor(fn) {
        // 根據之前描述構造器作用主要是運行傳入的fn
        // 將fn放在try catch中運行,防止fn執行出錯
        try {
            //new Promise((resolve, reject) => {})
            fn(this.resolve.bind(this), this.reject.bind(this))
        } catch(e) {
            this.reject(e)
        }
    }
    
    // resolve調用之後改變後promise的狀態,並且異步執行callback
    resolve(result) {
        // promise的狀態不是pending,說明該promise已經調用過reject/resolve
        if(this._status !== 'pending') throw new Error('重複決議promise')
        // 保存決議結果
        this._result = result
        // 異步執行callback
        this._runCallbackAsync('success')
    }
    
    // reject也是同理
    reject(err) {
        if(this._status !== 'pending') throw new Error('重複決議promise')
        this._result = err
        // 這裏我們執行錯誤回調
        this._runCallbackAsync('error')
    }
    
    ...
}

在寫promise.then之前我們重新考慮下這個方法到底做了什麼?我們逐步實現這個功能點。

  • then接收兩個函數,我們暫且稱它爲成功回調和錯誤回調,分別在fullFilled和rejected的時候執行
  • 當promise狀態已經是決議狀態時立即執行對應的成功回調/錯誤回調
  • 返回一個新的promise,新的promise在成功回調/錯誤回調執行完成後決議,並使用回調的返回作爲決議結果
class Promise {
    ...
    // 省略前後代碼
    
    then(fn1, fn2) {
        // 支持鏈式調用,立即回覆一個新的promise對象
        return new Promise((resolve, reject) => {
            /** 然後我們要將對應的回調函數推入事件隊列,他們將在本個promise
              * 決議後由_runCallbackAsync執行。
              * 但執行完對應事件後需要將執行結果傳到下一個promise中
              * 所以我們需要對回調函數進行小小的處理
              */
            
            // 接收決議結果
            const successCallBack = (result) => {
                try {
                    // 將promise的決議結果傳遞給回調函數去執行,並把回調函數的結果作爲下一個promise的決議結果
                    resolve(fn1(result))
                } catch(e) {
                    // 如果執行出錯,應該將錯誤信息傳遞給promise
                    reject(e)
                }
            }
            
            const errorCallback = (e) => {
                try {
                    reject(fn2(e))
                } catch(err) {
                    reject(err)
                }
            }
            
            // 將回調函數推入事件隊列
            if (fn1 && this._status !== 'error') this._successCallback.push(fn1)
            if (fn2 && this._status !== 'success') this._errorCallback.push(fn2)
            
            // 如果promise已經決議,則立即執行相應的回調
            if(this._status === 'success') this._runCallbackAsync('success')
            if(this._status === 'error') this._runCallbackAsync('error')
        })
    }
    
    // 實現
    _runCallbackAsync(type) {
        let eventQueue;
        if(type === 'error') eventQueue = this._errorCallback;
        else eventQueue = this._successCallback;
        // 執行回調, 使用settimeout模擬異步執行
        setTimeout(() => {
            eventQueue.forEach(callback => callback(this._result));
            // 清空事件隊列,兩個事件隊列都需要刪除,因爲promise決議後狀態不可變更,決議執行完應當清空所有隊列,以解除引用關係
            this._errorCallback.length = 0;
            this._successCallback.length = 0;
        }, 0)
    }
    ...
}

到此Promise的核心方法已經實現完畢,剩下的方法我們很容易使用現有方法進行實現,例如:

class Promise {
    ···
    catch(fn) {
        return this.then(undefined, fn);
    }
    
    static resolve(any) {
        // 使用鴨子類型去判斷any的類型
        if (
            typeof any.then === 'function'
            typeof any.catch === 'function'
            ...
        ) return any;
        // 如果不是一個promise則返回一個新的promise
        return new Promise((resolve) => {
            resolve(any)
        })
    }
    ···
}

總結

所以實現promise關鍵點在於,理解promise只決議一次,有三個狀態(pending,fullFilled,rejected),then/catch支持鏈式調用(返回一個新的promise),並且理解then/catch本質是個事件註冊器,類似於then = subscribe('resolve', callback),理解這些自己手動實現一個promise還是不難的。

最後的最後:剛開始寫技術文章,如果有錯漏希望能夠不吝指出,如果覺得寫的還不錯點個贊是對我最大的支持,謝謝。

最後附上源碼鏈接https://github.com/MinuteWong...

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章