前端面试之手写Promise

3,017 阅读5分钟

前言

最近总是听到前端面试要求手写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还是不难的。

最后的最后:刚开始写技术文章,如果有错漏希望能够不吝指出,如果觉得写的还不错点个赞是对我最大的支持,谢谢。

最后附上源码链接github.com/MinuteWong/…

欢迎阅读我的其他文章: