对 js 中 Promise 的细节探究,顺便纠正了 MDN 的一个小错误
在平常项目中,我们经常会使用到 promise,比如处理接口请求的时候,一般会用到的 axios,就是一个基于 promise 的网络请求库。看似简单的 promise 背后,其实也隐藏着诸多你知道或者不知道的细节,本文就是对它们的一个总结。
缘起
在 ES6 之前,官方规范里还没有 Promise 对象那会,如果我们想封装一个请求接口的方法,可能会是这样:
// 例 1
function request(succeedCallBack, failedCallBack) {
setTimeout(() => {
if (Math.round(10 * Math.random()) % 2) {
succeedCallBack('请求成功')
} else {
failedCallBack('请求失败')
}
}, 1000)
}
// 发送请求
request(
res => console.log(res),
err => console.log(err)
)
我们用一个定时器来模拟对接口的请求,用 Math.round(10 * Math.random()) % 2
的结果(0 或 1)来模拟请求的成功与失败。为了能在第 13 行发送请求后获取到请求结果,我们需要传入两个回调函数,在请求成功时执行 succeedCallBack()
,在请求失败时执行 failedCallBack()
。这样做似乎也没什么问题,但有 2 个不太方便的点:
- 我们定义 request 函数时还得费点脑细胞考虑该怎么设计回调函数的使用和名称;
- 如果别人也想调用 request,他必须来看看我这个函数是怎么定义的,他才知道该怎么用。
这就增加了我们的开发成本。从 ES6 开始,我们可以按照官方的规范,直接使用 Promise 对象,来封装一个异步操作并可以获取其结果,从而解决了上述的 2 点麻烦:
// 例 1.1
function request() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.round(10 * Math.random()) % 2) {
resolve('请求成功')
} else {
reject('请求失败')
}
}, 1000)
})
}
request()
.then(value => console.log(value))
.catch(reason => console.log(reason))
下面就来说说 promise 的一些细节问题。
executor
Promise 是个类,我们通过关键字 new ,并且传入一个处理器函数(executor function)来创建 Promise 实例对象。executor 本身是个回调函数,是个同步回调,就像数组遍历相关的回调函数那样,是立即执行的,等完全执行完了才会结束,不会放入回调队列中。executor 接收的参数也是两个回调函数 —— resolve
和 reject
,它们都是异步回调函数,不会立即执行,而是放入回调队列中将来执行。
Promise 的三种状态
Promise 对象有 3 种状态,且 promise 必然处于它们中的某一种状态:
- 当 executor 执行时,promise 处于 pending 状态;
- 如果在 executor 中执行的是
resolve()
,promise 就会变为 fulfilled 状态,表示异步操作成功完成。接着就会调用.then()
方法的 onFulfilled 回调函数; - 如果在 executor 中执行的是
reject()
,promise 就会变为 rejected 状态,表示异步操作完成失败。接着就会调用.then()
方法的 onRejected 回调函数或者是.catch()
方法。
一个 Promise 对象的状态只能改变一次,也就是说一旦调用了 resolve()
或 reject()
之后,状态就被锁定(settled),之后再次调用 reject()
或 resolve()
是不会改变 promise 状态的。下面放一张引用自 MDN 的图来说明 Promise 对象的基本流程 :
resolve 的参数
1. 普通的值或对象
一般情况下,传给 resolve()
的参数(value)都是一个普通的值或对象,然后触发 .then(onFulfillment)
,并且将该 value 传递给 then 方法。比如直接执行下面的例 2,将打印得到“成功了 我是返回的数据”:
// 例 2
new Promise((resolve, reject) => {
resolve({ msg: '我是返回的数据' })
}).then(
value => console.log('成功了', value.msg),
reason => console.log('失败了', reason)
)
但其实 resolve()
的参数还可以是其它的情况。
2. Promise 对象
如果传给 resolve()
的又是一个 Promise 对象:
// 例 2.1
new Promise((resolve, reject) => {
resolve(new Promise((resolve, reject) => {}))
}).then(
value => console.log('成功了', value.msg),
reason => console.log('失败了', reason)
)
此时外面这个 promise 的状态,将由这个传给 resolve
的 promise 的状态决定。比如例 2.1 我并没有在第 3 行的这个作为 resolve
参数的 promise 的 executor 中执行 resolve()
或 reject()
,所以里面这个 promise 的状态为 pending,故而外面这个 promise 的状态也为 pending,执行例 2.1 不会有任何打印。
如果让传给 resolve
的 promise 的状态变为 rejected,那么外面这个 promise 的状态也会变为 rejected ,接下去调用的会是 .then(onRejection)
,并且传递给 then 方法的值将会是第 5 行传给 reject()
的内容,即执行例 2.1.1 将打印“失败了 我是失败的理由”。
// 例 2.1.1
new Promise((resolve, reject) => {
resolve(
new Promise((resolve, reject) => {
reject('我是失败的理由')
})
)
}).then(
value => console.log('成功了', value.msg),
reason => console.log('失败了', reason)
)
3. thenable
如果传给 resolve()
的是一个 thenable,即一个带有 then 方法的对象。那么这个 then 方法会被执行,并且可以认为 resolve
和 reject
两个方法会传给该 then 方法,再根据该 then 方法的执行的是 resolve 或 reject 决定外面这个 promise 的状态:
// 例 2.1.2
new Promise((resolve, reject) => {
resolve({
then(resolve, reject) {
resolve('我是成功信息')
}
})
}).then(
value => console.log('成功了', value), // 成功了 我是成功信息
reason => console.log('失败了', reason)
)
执行例 2.1.2 将打印“成功了 我是成功信息”,可见第 9 行的 value 为传给第 5 行这个 resolve()
的值,而不是传给第 3 行这个 resolve()
的 thenable。事实上,then 方法的执行返回的还是一个 promise,所以本质上传给 resolve
的参数为 thenable 的情况和为 Promise 对象的情况是一样的。
实例方法
所谓实例方法指的是这些方法都是定义在 Promise 的原型对象 Promise.prototype 上的方法。
then
参数
在项目里我一般习惯只传给 then 方法一个当 promise 的状态变成 fulfilled 时会调用的回调函数 onFulfilled,然后用 catch 方法去处理 promise 被 rejected 的情况。但其实根据 Promises/A+ 规范,是没有 catch 方法的,promise 变为 rejected 后要调用回调应该是 then 方法的第二个参数 onRejected:
// 例 3
new Promise((resolve, reject) => {
// ...
}).then(
value => console.log('成功了', value), // onFulfilled
reason => console.log('失败了', reason) // onRejected
)
同一个 promise 可以被多次调用 then 方法
比如下面的例 3.1,我们定义了 2 次 p.then()
,由于第 3 行执行了 resolve()
, p 的状态变为 fulfilled,2 个 p.then()
的 onFulfilled 回调都会执行:
// 例 3.1
const p = new Promise((resolve, reject) => {
resolve()
})
p.then(
value => console.log('成功了1', value), // 成功了1 undefined
reason => console.log('失败了1', reason)
)
p.then(
value => console.log('成功了2', value), // 成功了2 undefined
reason => console.log('失败了2', reason)
)
传入 then 方法的回调函数的返回值
then 方法本身有个返回值,为一个新的 Promise 对象,所以可以再次链式调用 then 方法:
// 例 3.2
const p = new Promise((resolve, reject) => {
resolve()
})
p.then(() => {
return 'Jay'
}).then(value => console.log(value)) // Jay
例 3.2 中第 7 行打印结果之所以为“Jay”,与传入 then 方法的参数,也就是回调函数的返回值有关:
1. 普通值
如果返回值像例 3.2 中第 6 行那样是个普通的值,或者是个普通的对象,则第 5 行这个 then 方法返回的 promise 的状态就会变为 fulfilled,并且相当于会将返回的值作为参数传给新 promise 的 resolve。then 方法的实现相当于下面例 3.2.1 这样:
// 例 3.2.1
function then() {
return new Promise((resolve, reject) => {
resolve('Jay')
})
}
所以例 3.2 第 7 行链式调用的 then 方法,里面拿到的 value 值实际上相当于例 3.2.1 中的这个 promise 里传给 resolve 的值。
2. promise
如果返回值是一个 promise,那么就相当于传给例 3.2.1 第 4 行 resolve()
的值是一个 promise,那么此时的情况就和前面提到的 executor 中 resolve 参数的情况分析一样了。比如我们 return 的 promise 执行的是 reject()
,那么在之后链式调用的 then 方法里就会在第二个参数 onRejected 回调中获取到 reason:
// 例 3.2.2
p.then(() => {
return new Promise((resolve, reject) => {
reject('Zhou')
})
}).then(
value => console.log('value', value),
reason => console.log('reason', reason) // reason Zhou
)
3. thenable
如果返回的是一个 thenable,相当于给例 3.2.1 的 resolve 传入了 thenable,那么道理就和对 resolve 的参数分析的第 3 点是一样的:
// 例 3.2.3
p.then(() => {
return {
then(resolve, reject) {
resolve('亦黑迷失')
}
}
}).then(
value => console.log('value', value), // value 亦黑迷失
reason => console.log('reason', reason)
)
上面举的例子都是最开始这个 promise 的状态变为 fulfilled, 执行了 then 方法的 onFulfilled 回调。如果例 3.1 第 3 行执行的是 reject()
,即 promise 状态为 rejected,then 方法的回调执行的是 onRejected,那么其返回值的不同情况同样适用于上面的分析。比如下面的例 3.3,第 6 行返回了一个普通值,那么链式调用时会执行的是第 8 行的代码:
// 例 3.3
const p = new Promise((resolve, reject) => {
reject('错误原因')
})
p.then(undefined, reason => {
return 'Jay'
}).then(
value => console.log('value', value), // value Jay
reason => console.log('reason', reason)
)
catch
虽然在 Promises/A+ 规范中,关于拒绝(rejected )或错误状态的捕获回调函数,应该作为 then 方法的第二个参数。但是 ES6 的 Promise 实现了 catch 方法,使得代码书写起来更加清晰:
// 例 4
const p = new Promise((resolve, reject) => {
reject('错误原因')
})
p.then(undefined, reason => console.log(reason)) // 错误原因
p.catch(reason => console.log(reason)) // 错误原因
例 4 中的第 6 行使用 catch 来处理拒绝的情况,可以看成是第 5 行这种写法的语法糖。 如果在例 4 的第 3 行不是执行 reject 而是抛出了错误,同样是会执行 onRejected 回调:
// 例 4.1
const p = new Promise((resolve, reject) => {
throw new Error('抛出了错误')
})
p.catch(reason => {
console.log(reason)
console.log('之后的代码依旧可以执行')
})
在控制台可以看到错误打印信息:
因为只是打印了错误信息而不是报错,所以不会影响第 8 行代码的执行。 我们前面说过了,then 方法本身会返回一个新的 Promise 对象,但是像例 4.2 第 6 行这样的链式调用,catch 捕获的依然是 p 的拒绝:
// 例 4.2
const p = new Promise((resolve, reject) => {
reject('拒绝了')
})
p.then(value => console.log(value)).catch(reason => console.log(reason)) // 拒绝了
若 p 的状态变为 fulfilled,那么例 4.2 中的 catch 就会去捕获 then 方法返回的 promise 的拒绝或错误,如例 4.2.1:
// 例 4.2.1
const p = new Promise((resolve, reject) => {
resolve('Jay')
})
p.then(value => {
console.log(value) // Jay
return Promise.reject('拒绝了')
}).catch(reason => console.log(reason)) // 拒绝了
返回值
同 then 方法一样,catch 方法本身也是返回一个新的 Promise 对象,该 promise 的状态与传入 catch 的 onRejected 回调的返回值有关,判断方式与 then 方法的一样。下面的例 4.3 中,第 8 行返回的是一个字符串,那么 catch 返回的这个 promise 就会变为 fulfilled,所以会被调用的是第 10 行的 then 方法,而不是第 11 行的 catch:
// 例 4.3
new Promise((resolve, reject) => {
reject('拒绝')
})
.then(value => console.log(value))
.catch(reason => {
console.log(reason) // 拒绝
return '我是 catch 的参数 onRejected 回调函数的返回值'
})
.then(value => console.log('value', value)) // value 我是 catch 的参数 onRejected 回调函数的返回值
.catch(reason => console.log(reason))
finally
finally 方法比较简单,是 ES9(ES2018)添加的特性,在项目中我一般是在该方法的参数 onFinally 回调中写结束请求 loading 的语句。 onFinally 回调函数不接收参数。
类方法
Promise 除了有上面介绍的 3 种实例方法,还有 6 个类方法,也就是如果用 class 语法定义 Promise 时,用 static 关键字定义的方法。比如 Promise.resolve()
、Promise.reject()
、Promise.all()
和 Promise.allSettled()
等,都比较简单,就不多说了,仅就 ES12(ES2021)新增的 Promise.any()
方法做一些说明。
Promise.any()
如果我们想得到的是第一个变为 fulfilled 的 promise,忽略掉在它之前的为 rejected 的 promise,就可以使用 Promise.any()
方法:
// 例 5.5
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('Jay')
}, 2000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('拒绝')
}, 1000)
})
Promise.any([p1, p2])
.then(value => console.log('value', value)) // value Jay
.catch(reason => console.log('reason', reason))
如果传入 Promise.any()
的迭代器中每个 promise 的结果均为 rejected,那么其返回的 promise 就会变为 rejected,并且返回一个 ES12 新增的 Error 的子类 AggregateError 类型的实例,其有个 errors
属性,属性值是由所有失败值填充的数组:
// 例 5.5.1
const p1 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('拒绝1')
}, 2000)
})
const p2 = new Promise((resolve, reject) => {
setTimeout(() => {
reject('拒绝2')
}, 1000)
})
Promise.any([p1, p2])
.then(value => console.log('value', value))
.catch(reason => {
console.log(reason) // AggregateError: All promises were rejected
console.log(reason.errors) // ['拒绝1', '拒绝2']
})
请注意,目前 Promise.any()
方法依然是实验性的,尚未被所有的浏览器完全支持。另外,当时我去看 MDN 文档查看 Promise.any()
的内容,它说 AggregateError 有个 error 属性:
但是其实应该是 errors , 英文版就是正确的:
于是提交了个 pull request,现在已经改过来了:
转载自:https://juejin.cn/post/7242145254057426999