promise A+规范及源码编写

312 阅读11分钟

一、为什么需要promise?

javascript是一门单线程语言,所以早期解决js异步问题时,一般都采用传入callback回调函数的方式来解决。
例如 ajax请求就是最常见的js异步操作。我门需要在异步执行完成后,利用请求响应来进行下一步操作。如下利用setTimeout延时来模拟ajax异步请求,也就是请求1000毫秒完成以后,执行传入的callback回调函数并传入结果,完成异步处理。

function dynamicFunc(cb) {
    setTimeout(function() {
    	var response = 'hello' 
    	cb(response)
    }, 1000)
}

dynamicFunc(function(res) {
    console.log(res) // 'hello'
})

上面的写法看似没有问题,但是如果在回调函数中继续处理异步,继续传入回调,如果嵌套过多,就会导致我们的代码不是越写越长,而是越写越宽,这就会在js书写过程中的形成Callback Hell(回调地狱)。因此es新特性增加了promise规范及相应的Promise构造类的实现,基本解决了代码Callback Hell问题。

二、Promise构造函数的基础

以下是一段最简单的Promise使用代码:

function promise1() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var response = 'hello' 
            resolve(response)
        }, 1000)
    })
}

function promise2() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var response = 'hello2' 
            resolve(response)
        }, 2000)
    })
}
// 调用promise
promise1().then(function(res) {
    console.log(res) // 'hello'
    promise2()
}, function() {
	console.log('reject error')
})
// 或者简写
promise1().then(promise2)

从上面实现代码可以看出,通过new关键字创建一个promise实例对象,同时参数需要传递一个回调函数,这个回调函数同时又接收两个参数resolve和reject。当回调函数中的(异步或者同步)代码执行完后,可以通过resolve修改promise「已完成」状态,或者通过reject修改promise「已拒绝」状态。状态一旦修改,将再也无法改变。创建的promise实例,可以通过.then()方法,来处理promise状态改变(异步代码执行完)后的执行和接收状态传递的参数。then() 方法需要传入一个函数参数,这个函数就是promise状态改变后执行的函数,而这个函数接收的参数就是resolve传递的参数。

.then()方法还可以接收第二个参数,也是一个函数,这个函数可以捕获处理reject的拒绝状态,接收的参数也是reject执行传入的参数。当然除了利用.then()方法的第二个参数捕获已拒绝状态以外,还可以2种方式,一种是promise的.catch()方法,另一种是tryCatch异常处理。具体三种实现捕获异常的代码如下:

function promise() {
    return new Promise(function(resolve, reject) {
        setTimeout(function() {
            var flag = false
            if(flag) {
            	resolve('yes')
            } else {
            	reject('error')
            }
        }, 1000)
    })
}

// 1. .then() 的第二个参数捕获错误  注意:resolve不需要的话,第一个参数可以传null
promise().then(null, function(e) {
    console.log(e) // 'error'
})

// 2. .catch()
promise().catch(function(e) {
    console.log(e) // 'error'
}).then(null)

// 3. try catch 注意:这种方式不推荐  有些浏览器或者node环境,无法捕获到
try {
    promise().then(null)
} catch(e) {
    console.log(e) // 'error'
}

总结

  • promise有三种状态 进行中、已完成、已拒绝。进⾏中状态可以更改为已完成或已拒绝,已经更改过状态后⽆法继续更改(例如从已完成改为已拒绝)。
  • es6中的Promise构造函数,我们构造之后,需要传入一个回调函数,函数接收两个参数,resolve和reject。resolve会修改当前promise为「已完成」状态,reject会修改当前promise为「已拒绝」状态。
  • 对于已完成的promise,可以通过.then()的第一个参数方法进行捕获处理,即可在上一个promise达到已完成时,继续执行下一个函数或 promise。同时可以利用resolve继续往下传递参数。
  • 对于已拒绝的promise,可以通过.then() 的第二个参数捕获,也可以.catch()方法捕获,还可以try catch进⾏捕获。

三、(案例)利用Promise封装简单ajax

function ajaxAsync(method, url, data) {
    return new Promise(function(resolve, reject) {
        var xhr = new XMLHttpRequest()
        xhr.open(method || 'GET', url, true)
        xhr.onreadystatechange = function() {
            if(this.readyState !== 4) {
                return
            }
            if(this.status === 200) {
                resolve(this.response)
            } else {
                reject(new Error(this.statusText))
            }
        }
        xhr.send(data || null)
    })
}

// 使用 (可以先用catch捕获)
ajaxAsync('GET', 'https://...', null)
    .catch(function(err) {
        throw err
    })
    .then(function(response) {
        console.log(response) // ajax响应结果
    })

四、promise A+规范解读

任何符合promise规范的对象或函数都可以称之为promise,promise A plus规范地址:promisesaplus.com/
下面我们从规范层面明白promise的使用细节。

术语解读(先了解以下规范中的术语)

  • promise: promise是一个拥有then方法的对象或函数,其行为符合本规范。
  • thenable:定义了then方法的对象或函数
  • value:值。指任何javascript的合法值(包括 undefined,thenable,promise)。
  • exception:异常。是使用throw语句抛出的一个值(value)。
  • reason:原因。表示一个promise的拒绝原因的值(value)。

规范要求

1、promise的状态

promise在执行过程中,有且只有三种状态,进行中(Pending)、已完成(Fulfilled)、和已拒绝(Rejected)。 处于Pending状态时,可以变为Fulfilled或者Rejected。 处于Fulfilled状态时,不可变为其他任何状态,且必须拥有一个不可变的值(value)。 处于Rejected撞状态时,不可变为其他任何状态,且必须有一个不可变的原因(reason)。

2、必须有一个then方法

一个promise必须提供一个then方法去访问其当前的value或者reason。
一个then方法接受两个参数:promise.then(onFulfilled, onRejected),两个参数都是可选参数,且都是函数。如果onFulfilled或 onRejected传入的不是函数,则忽略它们(例如 传null就是不进行任何处理)。

如果onFulfilled是一个函数:
  • 当promise改变为已完成状态后该函数必须被调用,其第一个参数就是promise完成(resolve)的结果(值)。
  • promise执行完成前不可被调用
  • onFulfilled函数最多被调用一次
如果onRejected是一个函数:
  • 当promise改变为已拒绝状态后该函数必须被调用,其第一个参数就是promise拒绝(reject)的结果(原因)。
  • promise拒绝执行前不可被调用
  • onRejected函数最多被调用一次
在执行上下文堆栈仅包含平台代码之前,不得调用onFulfilled 或 onRejected。
onFulfilled和onRejected必须作为普通函数调用(即非实例化调用,这样非严格模式下,函数内部的this指向window)。
then方法可以实现链式调用
  • 当promise完成执行后,所有的onFulfilled需按照注册的顺序依次回调。
  • 当promise拒绝执行后,所有的onRejected需按照注册的顺序依次回调。
then方法必须返回一个promise对象(这也是上述可以链式调用的原因),promise2 = promise.then(onFulfilled, onRejected)。
  • 只要onFulfilled或者onRejected返回的是一个值x,那么返回的promise2都会进入Fulfilled状态。
  • 如果onFulfilled或者onRejected抛出一个异常e,则promise2必须拒绝执行,并返回异常e。
  • 如果onFulfilled不是函数且promise的状态变成已完成,那么返回的promise2必须进入已完成状态且返回promise返回的值。
  • 如果onRejected不是函数且promise的状态变成已拒绝,那么返回的promise2必须进入已拒绝状态且返回promise返回的原因。 例如:
var promise = new Promise(function (resolve, reject) {
    reject()
})
var promise2 = promise.then(null, function () {
    return 123
})

promise3 = promise2
.then(null, null)
.then(false, false)

promise3
.then(function (val) {
    console.log(val)
    console.log('promise3...fulfilled')
}, function (e) {
    console.log(e.message)
    console.log('promise3...rejected')
})

以上虽然promise是拒绝状态,但是在第一次onRejected捕获处理之后 返回的是值123,那么返回的promise2就是已完成状态的且返回的值就是123。然后已完成的promise2连续链式调用2次then,但是由于onFulfilled和onRejected都是null或者false,所以返回的promise3与promise2具有相同的完成状态和值123。所以最终打印结果是promise3...fulfilled。
那么假如第一次onRejected捕获处理之后返回的不是123,而是直接throw一个错误。那么最终结果一定就是promise3...rejected。

3、promise的解决过程

promise的解决过程是一个抽象的操作,我们需要修改promise的状态,并传入值x。尤其是promise执行then()方法后,需要返回一个新的promise2,新的promise2的状态完全由then()方法传入的onFulfilled或onRejected函数执行的返回值x决定。当然如果onFulfilled或onRejected不是函数,则会将promise的状态和值透传给返回给promise2。

如果x有then方法且看上去像一个promise,解决程序尝试使返回的promise接受x的状态,否则用x的值来执行返回的promise。

  • 如果x和返回的promise是同一个引用对象,则解决程序已TypeError的错误为拒绝原因来拒绝返回的promise。
  • 如果x为新的promise实例对象,则解决程序会一直等待这个promise状态修改为已完成或已解决,并且返回的promise最终接受这个promise的状态和值。
  • 如果x为object或function时(不常见),则首先尝试获取x.then, 如果抛出了一个异常错误e,则以抛出的错误为拒因来拒绝返回的promise。如果没有异常,则判断获取的x.then是不是一个函数方法,如果不是,则以普通值x为参数并修改返回的promise2为已完成状态;如果是一个函数,则将把x作为函数作用域的this来调用这个函数,并传入resolvePromise和rejectPromise两个参数(本质上就是直接传入了返回的promise2实例内部的resolve和reject方法),所以返回的promise2的状态完全都用户自己操作,如果执行了resolvePromise(y),则以值y为参数值修改promise2状态为已完成,如果执行了rejectPromise(r),则以值r为拒因修改返回的promise2状态为已拒绝。如果执行这个获取的x.then函数抛出异常e,则以e为据因拒绝返回的promise2。如果执行这个函数同时出现resolvePromise(y)、rejectPromise(r)、throw err三种其中的任意的组合。则谁先执行,就按最先执行的状态修改返回的promise2状态。如果在x.then这个三种都没有执行,则promise2一只会处于pending状态。(说明x.then为函数时,返回的promise2状态必须由用户自己去执行修改)。
  • 如果x不为对象或者函数,以x为参数将返回的promise2变为已完成状态(重要且常⻅)

五、Promise构造函数上的静态⽅法

Promise.resolve(xx)

返回⼀个promise实例,参数xx等同于前面规范解读中的返回值x,也就是说返回的promise实例状态有参数xx决定。(即promise解决过程),所以Promise.resolve(xx) 不一定返回已完成状态的promise实例。

Promise.reject(r)

会直接返回一个已拒绝的promise实例,拒绝原因为参数r。

Promise.all([promise, ..., promise])

返回一个promise实例,只有当参数数组中的所有promise对象都变为已完成状态时,返回的promise才是变为已完成状态,那么已完成状态的值就为所有promise参数完成的值的数组组合,且一一对应; 否则如果有一个拒绝了,返回的promise就是已拒绝的状态。拒绝状态的原因为参数promise中第一个拒绝的值。例如:

var promise1 = new Promise((resolve, reject) => {
    setTimeout(() =>{
        resolve('promise1')
    }, 1000)
})
var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('promise2')
    }, 2000)
})

var promise3 = Promise.all([promise1, promise2])

promise3.then((val) => {
    console.log(val) // [ 'promise1', 'promise2' ]
}, (e) => {
    console.log(e)
})

Promise.race([promise, ..., promise])

返回一个promise实例,参数数组中的promise是竞争关系,谁最先修改状态(已完成或已拒绝),返回的promise将继承这个最先修改的状态和值(或原因)。例如:

var promise1 = new Promise((resolve, reject) => {
    setTimeout(() =>{
        reject(new Error('promise1 error...'))
    }, 0)
})
var promise2 = new Promise((resolve, reject) => {
    setTimeout(() => {
        resolve('promise2 fulfilled')
    }, 2000)
})

var promise3 = Promise.race([promise1, promise2])

promise3.then((val) => {
    console.log(val)
}, (e) => {
    console.log(e.message) // promise1 error...
})

六、基于promiseA+规范 手写一个CustomPromise

function CustomPromise (handleFunc) {
    this._init(handleFunc)
}
CustomPromise.prototype = {
    constructor: CustomPromise,

    _init(handleFunc) {
        this.status = 'pending'
        this.value = undefined
        this.bindFulfilledList = []
        this.bindRejectedList = []
        handleFunc(this._triggerResolve.bind(this), this._triggerReject.bind(this))
    },
    _triggerResolve(value) {
        var that = this
        queueMicrotask(function () {
            if (that.status !== 'pending') return
            that.status = 'fulfilled'
            that.value = value
            that.bindFulfilledList.forEach(bindFulfilled => bindFulfilled(value))
            that.bindFulfilledList = []
        })
    },
    _triggerReject(reason) {
        var that = this
        queueMicrotask(function () {
            if (that.status !== 'pending') return
            that.status = 'rejected'
            that.value = reason
            that.bindRejectedList.forEach(bindRejected => bindRejected(reason))
            that.bindRejectedList = []
        })
    },
    then(onFulfilled, onRejected) {
        var that = this
        var promiseInstance = new CustomPromise(function(nextResolve, nextReject) {
            function bindFulfilled (value) {
                if (typeof onFulfilled !== 'function') {
                    nextResolve(value)
                } else {
                    try {
                        var res = onFulfilled(value)
                        disposeRes(res)
                    } catch (error) {
                        nextReject(error) 
                    }
                }
            }

            function bindRejected (reason) {
                if (typeof onRejected !== 'function') {
                    nextReject(reason)
                } else {
                    try {
                        var res = onRejected(reason)
                        disposeRes(res)
                    } catch (error) {
                        nextReject(error) 
                    }
                }
            }

            function disposeRes (res) {
                if(res === promiseInstance) {
                    nextReject(new TypeError('Chaining cycle detected for promise'))
                    return
                }
                if(res instanceof CustomPromise) {
                    res.then(function (val) {
                        nextResolve(val)
                    }, function (e) {
                        nextReject(e)
                    })
                    return
                }
                if(typeof res === 'object' || typeof res === 'function') {
                    var then = res.then

                    if(typeof then === 'function') {
                        then.call(res, nextResolve, nextReject)
                    } else {
                        nextResolve(res)
                    }
                    return
                }
                nextResolve(res)
            }

            switch(that.status) {
                case 'pending':
                    that.bindFulfilledList.push(bindFulfilled)
                    that.bindRejectedList.push(bindRejected)
                    break
                case 'fulfilled':
                    bindFulfilled(that.value)
                case 'rejected':
                    bindRejected(that.value)
                default:
                    break
            }
        })
        return promiseInstance
    },
    catch(onRejected) {
        return this.then(null, onRejected)
    }
}

CustomPromise.resolve = function (value) {
    return new CustomPromise(function (resolve, reject) {
        resolve()
    }).then(() => value)
}

CustomPromise.reject = function (reason) {
    return new CustomPromise(function (resolve, reject) {
        reject(reason)
    })
}

CustomPromise.all = function (list) {
    return new CustomPromise(function (resolve, reject) {
        if (list.length < 1) {
            resolve([])
        } else {
            let array = new Array(list.length)
            let count = 0
            for(let [i, promiseInstance] of list.entries()) {
                promiseInstance
                    .then(
                        res => {
                            array[i] = res
                            count++
                            if(count === list.length) {
                                resolve(array)
                            }
                        },
                        e => {
                            reject(e)
                        }
                    )
            } 
        }
    })
}

CustomPromise.race = function (list) {
    return new CustomPromise(function (resolve, reject) {
        if(list.length < 1) return
        for (let [i, promiseInstance] of list.entries()) {
            promiseInstance
                .then(
                    res => {
                        resolve(res)
                    },
                    e => {
                        reject(e)
                    }
                )
        }
    })
}