promise 简介
promise 是异步编程的一种解决方案,解决了回调地狱的问题,现在已经是 JavaScript 和 DOM 提供异步返回值的正式方法。所有未来的异步 DOM API 都会使用它们。
promise 与回调地狱
回调地狱会造成严重的信任问题,比如有时候调用 ajax(..),属于某个第三方提供的工具,不是你编写的代码,也不在你的直接控制下。这称为控制反转,也就是把自己程序一部分的执行控制交给某个第三方。而promise实现的是控制反转的反转,它实现的是:希望第三方给我们提供了解其任务何时结束的能力,然后由我们自己的代码来决定下一步做什么,可能上一步成功,也有可能上一步失败了。
建议:关于异步与promise的详细内容大家可以翻阅一下**《你不知道的JS(中卷)》**
promise 实现
实现参考
通过阅读A+规范,里面有更加具体的promise定义,如下
- “promise” is an object or function with a
thenmethod whose behavior conforms to this specification.- “thenable” is an object or function that defines a
thenmethod.- “value” is any legal JavaScript value (including
undefined, a thenable, or a promise).- “exception” is a value that is thrown using the
throwstatement.- “reason” is a value that indicates why a promise was rejected.
字都认识的情况下还是翻译一下吧。
promise具有一个then方法promise有一个value值,代表决议为完成的值promise有一个reason值,给出了promise决议结果为rejected的理由
继续阅读规范中的requirements,可以发现实现promise就相当于实现每条规则,真正可扩展(大显身手)的空间并不多。下面就让我们按照A+规范一步步实现吧!
具体实现
part 1
promise定义的第一条就给出了promise是一个对象或函数,这里我们使用class类来实现,本质上都是一样的。在类的构造函数中需要传递一个执行器,它将立即执行。这个执行器需要传入两个参数(决议函数):
resolve:异步操作执行成功后的回调函数,标识完成;reject:异步操作执行失败后的回调函数,标识拒绝
class MyPromise{
constructor(excutor){
excutor(this.resolve(), this.reject())
}
resolve = () => {
}
reject = () => {
}
}
module.exports = MyPromise
有了决议函数,我们该思考,决议函数具体该怎么做来标识完成或拒绝。简单来说,决议函数所做的就是更改状态。在规范中对promise的状态做出了非常详细的规定。
ok,继续翻译一下😁
- 这里有三种状态,包括:
- 成功
fulfilled - 失败
rejected - 等待
pending
- 状态之间的转化
pending->fulfilledpending->rejected
const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
class MyPromise{
constructor(excutor){
excutor(this.resolve(), this.reject())
}
status = PENDING
resolve = () =>{
//如果状态不是等待,return,因为只有pending状态能转化为其他两种状态
if (this.status != PENDING) return
// 更改状态为成功
this.status = FULFILLED
}
reject = () =>{
if (this.status != PENDING) return
// 更改状态为失败
this.status = REJECTED
}
}
then方法
A promise must provide a
thenmethod to access its current or eventual value or reason.
promise.then(onFulfilled, onRejected)
onFulfilled标识状态决议为fulfilled后的回调,onRejected表示状态决议为rejected后的回调。
then (onFulfilled, onRejected){
if(this.status === FULFILLED){
onFulfilled()
}else if(this.status === REJECTED){
onRejected()
}
}
前面规范中提到的value和reason需要对应传递给onFulfilled和onRejected,那么value和reason从哪来的,其实就是用户传入的,比如下面传入的就是success。
new Promise((resolve, reject) => {
resolve('success')
})
所以要在resolve和reject中先保存一下value和reason值。
// 成功之后的值
value = undefined
// 失败之后的原因
reason = undefined
resolve = (value) => {
if (this.status != PENDING) return
this.status = FULFILLED
// 保存成功之后的值
this.value = value
}
reject = (reason) => {
if (this.status != PENDING) return
this.status = REJECTED
// 保存失败后的原因
this.reason = reason
}
then(onFulfilled, onRejected){
if (this.status === FULFILLED) {
onFulfilled(this.value)
} else if (this.status === Rejected) {
onRejected(this.reason)
}
}
到这里,我们可以测试一下,看看输出什么吧!
let promise = new MyPromise((resolve, reject) => {
resolve('success')
reject('fail')
})
promise.then(value =>{
console.log(value)
},reason => {
console.log(reason)
})
part 2
前面已经实现出一个promise雏形了,现在then方法中只判断了FULFILLED和Rejected两种状态,但当含有异步操作时,这时未决议出结果,状态为PENDING,因此需要在then方法中处理异步的操作。
当判断状态为PENDING时,先将成功和失败回调存储起来,等到有了结果之后再调用,因此定义两个存储回调函数的数组,为什么是数组,这是因为同一个promise的then方法是可以多次调用的,也就是不止传入一个成功或失败回调。
// 成功回调
onFulfilleds = []
// 失败回调
onRejecteds = []
resolve = (value) => {
...
// 异步代码执行完成后判断是否存在成功回调
while (this.onFulfilleds.length) this.onFulfilleds.shift()(this.value)
}
reject = (reason) => {
...
// 异步代码执行完成后判断是否存在失败回调
while (this.onRejecteds.length) this.onRejecteds.shift()(this.reason)
}
then(onFulfilled, onRejected){
if (this.status === FULFILLED) {
onFulfilled(this.value)
} else if (this.status === Rejected) {
onRejected(this.reason)
} else {
// 先将回调函数存储起来
this.onFulfilleds.push(onFulfilled)
this.onRejecteds.push(onRejected)
}
}
到这里可以简单的测试一下,看看是不是3s后输出的 success。
let promise = new MyPromise((resolve, reject) => {
setTimeout(() => {
resolve('success')
},3000)
})
promise.then(value =>{
console.log(value)
},reason => {
console.log(reason)
})
promise.then(value =>{
console.log(value)
},reason => {
console.log(reason)
})
以上我们多次调用了同一个promise的then方法,得到了期望的结果,但promise并不只是一个单步执行 this-then-that 操作的机制,我们可以把多个 promise 连接到一起以表示一系列异步,实现的核心就是链式流,它依赖于promise的几个固有特性:
• 调用 Promise 的 then(..) 会自动创建一个新的 Promise 从调用返回。
• 在完成或拒绝处理函数内部,如果返回一个值或抛出一个异常,新返回的(可链接的)Promise 就相应地决议。
• 如果完成或拒绝处理函数返回一个 Promise,它将会被展开,这样一来,不管它的决议值是什么,都会成为当前 then(..) 返回的链接 Promise 的决议值。
链式流程控制的实现就靠then方法啦,它将this-then-that转化成了this-then-this-then-this...,由promise链式流相关的固有特性得出实现then方法链式调用的几个方面:
then方法要返回一个全新的promise对象,能支持链式调用- 后面的
then方法实际上在为上一个then返回的promise注册回调 - 前面
then方法中回调函数的返回值会作为后面then方法回调的参数 - 若回调中返回的是
promise,后面then方法的回调也会等待它的决议结果
then(onFulfilled, onRejected){
//创建一个新的promise对象,支持链式调用
let promise2 = new MyPromise((resolve, reject) => {
if (this.status === FULFILLED) {
// 这里为什么将其变成异步任务呢,因为这时候直接式获取不到promise2的
setTimeout(() => {
//拿到当前回调函数的值,用来传递给下一个then
let x = onFulfilled(this.value)
// 判断 x 的值是普通值还是promise对象
// 如果是普通值,直接调用resolve
// 如果是promise对象查看promise对象返回的结果
// 根据返回的结果,决定调用resolve还是reject
resolvePromise(promise2, x, resolve, reject)
}, 0)
} else if (this.status === Rejected) {
setTimeout(() => {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
}, 0)
} else {
// 先将回调函数存储起来
this.onFulfilleds.push(() => {
setTimeout(() => {
try {
let x = onFulfilled(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0);)
this.onRejecteds.push(() => {
setTimeout(() => {
try {
let x = onRejected(this.reason)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)
})
}
})
return promise2
}
function resolvePromise(promise2, x, resolve, reject) {
// 调用自身,造成了promise的循环调用
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'))
}
if (x instanceof MyPromise) {
//promise对象
x.then(resolve, reject)
} else {
resolve(x)
}
}
写到这可以测试一下。
then的参数
想想下面输出什么🤔
let promise = new Promise((resolve,reject)=>{
resolve('success')
})
promise.then().then().then(value=>{console.log(value)})
在实际使用中,then方法的参数是可选的,但我们需要保证链式控制流程正常,也就是能够一直传递下去。因此需要对传参处理一下。
then(onFulfilled, onRejected){
// then中也可以不传递参数,但是要保证能够一直传递下去,直到传递给有参数的
onFulfilled = onFulfilled ? onFulfilled : value => value
onRejected = onRejected ? onRejected : reason => { throw reason }
}
不过这样还是有问题的,因为promise中还规定了当then传递非函数值应该替换为相应的默认回调,因此应判断下参数类型是否为函数。
then(onFulfilled, onRejected){
// then中也可以不传递参数,但是要保证能够一直传递下去,直到传递给有参数的
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }
}
part 3
为了代码的健壮性,我们需要添加一些错误捕捉try-catch,在失败回调中能够捕获到。
- 捕获执行器的异常
try {
executor(this.resolve, this.reject)
} catch (e) {
this.reject(e)
}
- 捕获
then回调函数中发生的错误,在下一个then的失败回调函数中捕获到
setTimeout(() => {
try {
let x = successCallback(this.value)
resolvePromise(promise2, x, resolve, reject)
} catch (e) {
// 把这个错误传给下一个promise的回调函数
reject(e)
}
}, 0);
到这里,一个简单的promise就实现完了。至于其他API,包括Promise.all, Promise.race等等,大家可以自己实现一下,完整代码中也有。
总结
- 在实现之前,需要先掌握
promise怎么用,如果连使用都不清楚的话,实现也没有意义 - 按照A+规范一步步实现,每实现一步测试一下,这可不是一蹴而就的过程
- 实现完成后可以用
promises-aplus-tests去测试实现的promise是否完全符合A+规范 - 完整代码请戳