莴莴莴笋

实现一个最简单的 Promise

一、前言

Promise 是 ES6 新增的一个内置对象,用来避免回调地狱的一种解决方案,相比回调函数,使用Promise 处理异步流程可以使代码层次清晰,便于理解,配合 async/await 语法糖更是直接以同步的方式写异步代码,再也不用担心回调地狱了。

Promise 有3种状态:pending、fulfilled、rejected。一个 Promise 最初的状态是 pending,在 resolve 被调用时变为 fulfilled,或者在 reject 被调用时变为 rejected。可以简单理解为 Promise 就是一个发布订阅模型,通过 then 方法注册状态改变时的回调函数,当 Promise 的状态改变时调用注册的回调函数并返回新的 Promise。

目前 Promise 的主流规范主要是 Promises/A+,对照标准就可以实现一个完整的 Promise,本文只是对 Promise 的简单实现,复杂情况暂时先不考虑。

二、内部属性

Promise 的主要逻辑就是对其内部状态的维护,要实现一个 Promise,需要定义一些属性来保存状态。

function Promise () {
    this.status = 'pending'
    this.value = undefined
    this.onFulfilledCallback = []
    this.onRejectedCallback = []
}

三、构造函数

Promise 的构造函数接受一个带有 resolve 和 reject 两个参数的函数,先把它称之为 executor,Promise 构造函数执行时立即调用 executor 并传递 resolve 和 reject 两个函数作为参数,resolve 和 reject 函数被调用时,会分别将 Promise 的状态改变为 fulfilled 或者 rejected,如果 executor 中抛出一个错误,Promise 状态也会变为 rejected。了解了这些,接下来继续完善 Promise 的构造函数。

function Promise (executor) {
    this.status = 'pending'
    this.value = undefined
    this.onFulfilledCallback = []
    this.onRejectedCallback = []
    try {
				executor(this.resolve.bind(this), this.reject.bind(this))
    } catch (e) {
        this.reject(e)
    }
}

浏览器中的 resolve 和 reject 函数是使用原生代码在JavaScript虚拟机中实现的,这里需要我们自己实现。resolve 和 reject 主要的功能就是改变Promise的状态,然后触发回调函数,这里我们把 resolve 和 reject 定义在 Promise 的原型上。

Promise.prototype.resolve = function(value) {
    if (this.status !== 'pending') return
    this.status = 'fulfilled'
    this.value = value
    this.onFulfilledCallback.forEach(function(cb) {
		    cb(value)
    })
}
Promise.prototype.reject = function(reason) {
    if (this.status !== 'pending') return
    this.status = 'rejected'
    this.value = reason
    this.onRejectedCallback.forEach(function(cb) {
		    cb(reason)
    })
}

四、then 和 catch 方法

then 方法用来注册 Promise 状态确定后的回调,then 方法最多需要 onFulfilled, onRejected 两个参数:分别对应 Promise 的成功和失败情况的回调函数,并返回一个新的Promise。对应不同的状态,调用 then 方法执行的逻辑也不一致。

  • fulfilled: 返回一个新的 Promise,在新 Promise 的 executor 中调用 onFulfilled,并立即调用新Promise 的 resolve 函数。
  • rejected:和 fulfilled 类似
  • pending:这是平时使用过程中最常见情况,旧 Promise 的执行结果还未确定,分别将处理代码注册为回调函数:onFulfilledCallback、onRejectedCallback,等到旧 Promise 的结果确定后调用。

根据规范:如果 onFulfilled 或 onRejected 的返回结果是 Promise,那么 then 返回的 Promise 的状态与之相同。

Promise.prototype.then = function(onFulfilled, onRejected) {
    var _this = this
    var defaultFn = function(v) { return v} 
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : defaultFn
    onRejected = typeof onRejected === 'function' ? onRejected : defaultFn

    if (_this.status === 'fulfilled') {
        return new Promise(function(resolve, reject) {
            var ret = onFulfilled(_this.value)
            if (ret instanceof Promise) {
                ret.then(resolve, reject)
            } else {
                resolve(ret)
            }
        })
    }
    if (_this.status === 'rejected') {
        return new Promise(function(resolve, reject) {
            var ret = onRejected(_this.value)
            if (ret instanceof Promise) {
                ret.then(resolve, reject)
            } else {
                reject(ret)
            }
        })
    }
    if (_this.status === 'pending') {
        return new Promise(function(resolve, reject) {
            _this.onFulfilledCallback.push(function(value) {
                try {
                    var ret = onFulfilled(value)
                    if (ret instanceof Promise) {
                      ret.then(resolve, reject)
                    } else {
                      resolve(ret)
                    }
                  } catch (e) {
                    reject(e)
                }
            })

            _this.onRejectedCallback.push(function(reason) {
                try {
                    var ret = onRejected(reason)
                    if (ret instanceof Promise) {
                        ret.then(resolve, reject)
                    } else {
                        resolve(ret)
                    }
                } catch (e) {
                    reject(e)
                }
            })
        })
    }
}

顺便实现 catch 方法

Promise.prototype.catch = function(onRejected) {
    return this.then(null, onRejected)
}

五、执行时机

Promise 构造函数中传入的 executor 函数会立即执行,但是 then 方法中传入的 onFulfilled, onRejected 函数需要异步延迟调用,Promise/A+ 规范中解释:实践中要确保 onFulfilled 和 onRejected 方法异步执行,且应该在 then 方法被调用的那一轮事件循环之后的新执行栈中执行。这个事件队列可以采用宏任务 macro-task 机制或微任务 micro-task 机制来实现。

因此即便一个 promise 立即被 resolve,在 then 之后的代码也会在 onFulfilled, onRejected 前面执行,如下例打印结果是1,3,2 而不是 1,2,3 。

let promise = new Promise((resolve, reject) => {
    console.log(1)
    resolve()
})
promise.then(() => {
    console.log(2)
})
console.log(3)
// 打印结果
// 1
// 3
// 2

在浏览器中 onFulfilled 和 onRejected 的异步执行是使用微任务机制实现的,我们使用宏任务setTimeout 来实现异步执行,稍微修改一下 resolve、reject 的实现代码。

Promise.prototype.resolve = function(value) {
    var _this = this
    if (_this.status !== 'pending') return
    setTimeout(function() {
        _this.status = 'fulfilled'
        _this.value = value
        _this.onFulfilledCallback.forEach(function(cb) {
            cb(value)
        })
    })
}
Promise.prototype.reject = function(reason) {
    var _this = this
    if (_this.status !== 'pending') return
    setTimeout(function() {
        _this.status = 'rejected'
        _this.value = reason
        _this.onRejectedCallback.forEach(function(cb) {
            cb(reason)
        })
    }, 0)
}

六、测试

let promise = new Promise((resolve, reject) => {
    console.log('start')
    setTimeout(() => {
        resolve(1)
    }, 1000)
})
promise.then(v => {
    console.log(v)
    return v + 1
}).then(v => {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            console.log(v)
            resolve(v + 1)
        }, 1000)
    })
}).then(v => {
    console.log(v)
    return v + 1
})

打印结果

start
1
2
3