前言 🎙
大家好,我是潘小安!一个永远在减肥路上的前端er🐷 !
这篇文章是上一篇 手写 Promise 的补充部分🩹,主要是结合 demo 和大家聊聊Promise 常用的一些静态方法和异常处理,从而让我们在日常开发中更好的去理解和使用 Promise。话不多说(别的事留在小声 BB 章节聊),让我们从静态函数开始整!
静态函数 🤐
按照我的理解,静态函数总体可以分为两部分,一部分为 Promise 封装方法,包括 Promise.resolve 和 Promise.reject。另一部分为 Promise 批处理方法,包括 Promise.all、Promise.allSettled、Promise.any、Promise.race,我们先从封装方法 Promise.resolve 开始聊。
Promise 封装方法
Promise.resolve
Promise.resolve 返回一个解析过的 Promise 对象。
Promise.resolve方法参数分为三种情况,分别对应三种不同的返回值:
| 参数 | 返回值 |
|---|---|
| Promise 实例 | 返回参数本身 |
| thenable 对象 | 展平 thenable 对象并返回 |
| 其他 | 返回一个以参数为完成结果的 Promise 实例 |
我们可以通过几个 demo 来辅助理解 Promise.resolve 方法对不同参数如何处理:
参数为 promise:
let param = new Promise((resolve, reject) => {
resolve('param is resoved promise')
})
let p = Promise.resolve(param)
console.log(p === param)//true
参数为 thenable 对象
(对于 thenable 对象的一些吐槽可以翻阅《你不知道的 JS 中卷》第 188 页,文末会直接给到链接)
因为我们日常使用 promise的时候,很少使用 thenable 对象,所以我们在写 demo 之前,先来看看 thenable 对象的定义:
thenable:任何含有then()方法的对象或函数.
参考手写 Promise 中的 resolvePromise 代码,我们可以知道,如果想让 promise 链得到结果,thenbale 对象需要长下面这样:
let thenable = {
then: (resolve, reject) => {
resolve('1')
}
}
如果对 then 方法中的 resolve 和 reject 感到困惑的小伙伴,可以看看点这里跳转到《手写 Promise》中 resolvePromise 方法内,当 x 为 object 或者 function,且 x.then 存在的代码,相信可以解答你的疑惑。
我们知道 thenable 作为参数的时候,Promise.resolve 会展平 thenable 对象并采用它的最终状态,考虑下面这种情况:
let thenable = {
then: (resolve, reject) => {
onFulfill({
then: (resolve, reject) => {
resolve('1')
}
})
}
}
let p = Promise.resolve(thenable)
console.log(p)//promise:{status:fulfilled,result:1}
所以我们需要避免在 thenable 的 then 方法中,使用 thenable 本身作为参数来调用 resolve 方法,因为这样会导致无限递归,考虑下面这种情况:
let thenable = {
then: (resolve, reject) => {
resolve(thenable)
}
}
Promise.resolve(thenable) // 死循环
这里需要注意的是,采用 thenable 的最终状态,包含了 thenable 抛出异常的情况,我们可以使用下面的 demo 来测试这种情况:
let thenable = {
then: (resolve, reject) => {
throw new Error('I am an erro')
}
}
let p = Promise.resolve(thenable)
console.log(p)//Promise:promiseStatus:rejected,promiseResult:Erro,I am an erro
参数为其他值
当参数为其他值时,Promise.resolve 方法会返回以参数为 promiseResult,promiseStatus 为 fulfilled 的 Promise 实例。我们可以使用不同的基本类型写测试 demo 并查看结果:
let p=Promise.resolve(1)
let p1=Promise.resolve('1')
let p2=Promise.resolve(null)
let p3=Promise.resolve(undefined)
let p4=Promise.resolve(true)
console.log(p)
console.log(p1)
console.log(p2)
console.log(p3)
console.log(p4)
打印结果如下:
手写 Promise.resolve()
有了上面的 demo 结果辅助理解,我们可以尝试手写一下 Promise.resolve 的源码:
Promise.resolve = function (value) {
// 处理参数为 Promise 的情况
if (value instanceof Promise) return value;
// 处理其他情况
return new Promise((resolve) => {
resolve(value);
});
}
当参数为 Promise 实例,返回实例本身,否则返回一个新的 Promise 实例,参数作为值被 resolve 调用,结合之前 手写 Promise 的 resolve 方法补充,resolve 方法会自动去展开 thenbale。
Promise.reject
Promise.reject() 返回一个带有拒绝原因的 Promise 对象。
手写 Promise.reject()
Promise.reject() 方法比较好理解,就是返回一个结果拒绝原因 reason,状态为 rejected 的 Promise 实例。我们可以尝试写出它的实现源码:
Promise.reject = function (reason) {
return new Promise((resolve, reject) => {
reject(reason)
})
}
Promise 批处理方法
Promise 实例的批处理方法 的共同点就是参数为一堆 Promise 的实例,返回值是一个新的 Promise,返回值取决于参数中各个 Promise 实例的状态和结果;不同点在于规则,四个方法对参数中的多个 promise 的状态和结果有不同的规则,总结一下可以通俗的理解成以下表格(为了方便理解,将 promise 的状态由 pending=>fulfilled 为对,pending=>rejected 为错):
| 方法 | 通俗理解返回值 |
|---|---|
| Promise.all | 全对则对,错一个就错 |
| Promise.allSettled | 不管对错,全执行完后返回每个的状态和结果 |
| Promise.race | 不管对错,谁先执行完就返回谁 |
| Promise.any | 有一个对就返回,全错才返回错 |
接下来我们就从 Promise.all 方法开始,深入的去了解每个方法的使用细节和源码实现。
Promise.all
Promise.all 有如下规则:
- 参数需要为可迭代对象
iterable,如Array或Sring,否则抛出类型错误。 - 可迭代对象为空,则同步返回一个状态为
fulfilled的Promise实例。 - 可迭代对象不为空,但是不包含
Promise实例,则异步返回一个fulfilled的Promise实例。 - 可迭代对象不为空,且包含
Promise实例,则返回一个pending状态的Promise实例,该实例的状态变更条件为:- 所有参数中的
Promis实例从pending状态变成fulfilled状态,返回值从pending状态变成fulfilled状态 - 参数中有一个 Promise 实例从 pending 状态变成 rejected 状态,返回值从 pending 状态变成 rejected 状态
- 所有参数中的
- 返回值将会按照参数内的
promise顺序排列,而不是由调用promise的完成顺序决定。
接下来我们使用 demo 来验证一下这些规则:
//参数需要为可迭代对象
let p = Promise.all(1)
console.log(p)//number 1 is not iterable
//参数为迭代对象,但是为空,同步返回一个 Promise 实例,状态为 fulfilled
let p = Promise.all([])
console.log(p)//promise:{promiseState:fulfilled,promiseResult:[]}
//参数为迭代对象,但是不为空,不包含 Promise 实例,异步返回一个 Promise 实例,状态为 fulfilled
let p = Promise.all([1, 2, 3])
console.log(p)//promise:{ promiseState:pending }
setTimeout(function () {
console.log(p)//promise:{promiseState:fulfilled,promiseResult:[1,2,3]}
})
//可迭代对象不为空,且包含 Promise 实例,则返回一个 pending 状态的 Promise 实例,状态根据参数中的 promise 的变更情况变更。
//参数中状态都变成 fulfilled 的情况
var p1 = Promise.resolve(1);
var p2 = Promise.resolve(2);
var p3 = Promise.resolve(3);
let pall = Promise.all([p1, p2, p3])
console.log(pall)//promise:{ promiseState:pending }
setTimeout(function () {
console.log(pall)//promise:{promiseState:fulfilled,promiseResult:[1,2,3]}
}
)
//参数中存在 rejected 状态的 promise
var p1 = Promise.resolve(1);
var p2 = Promise.reject(2);
var p3 = Promise.resolve(3);
let pall = Promise.all([p1, p2, p3])
console.log(pall)//promise:{ promiseState:pending }
setTimeout(function () {
console.log(pall)//promise:{promiseState:rejected,promiseResult:2}
}
)
最后一个测试用例我们单独列出来查看:
let p1 = new Promise((resolve, reject) => {
resolve('1')
})
let p2 = new Promise((resolve, reject) => {
resolve(new Promise((resolve, reject) => {
setTimeout(function () {
resolve('3')
})
}))
})
let result = Promise.all([p1, p2, 3, {
then: (resolve, reject) => {
resolve('4')
}
},])
console.log(result)//promise:{ promiseStatus:pending }
setTimeout(function () {
console.log(result)//promise:{promiseStatus:fulfilled,promiseResult:["1","2","3",4,"5"]}
})
在这个 demo 中,参数有以下特点:
- 数组第一个变量
p1:一个状态为fulfilled的Promise实例,promiseResult为 1 - 数组第二个变量
p2:嵌套promise,且嵌套定时器,最终的结果为状态为fulfilled的Promise实例,promiseResult的值为 2 - 数组第三个变量:数字 3
- 数组第四个变量:一个
thenable对象 根据打印出来的结果,我们可以得到一个新的规则,即:
如果参数中的
promise实例的promiseResult还是一个Promise实例 或者thenable对象,会被展开。
通过以上这些 demo,我们可以尝试来手写一下 Promise.all 的代码,代码中每段逻辑都使用注释标记.
Promise.all = function (params) {
if (!(typeof params[Symbol.iterator] === 'function')) {
throw new TypeError('params is not an iterator')
}
return new Promise((resolve, reject) => {
//参数为空的时候,直接返回状态为 fulfilled 的 promise,result 为[]
const final=[]
let len = args.length;
if (len === 0) return resolve(final);
for (let i = 0; i < params.length; i++) {
const item = params[i];
//非 promise 的转换成 promise,then 方法可以展开所有的 promise 的嵌套
Promise.resolve(item).then((result) => {
final[i] = result;
if (--len === 0) {
resolve(final);
}
}, (reason) => {
// 一旦有promise被拒绝就立即拒绝
reject(reason);
});
}
});
};
Promise.allSettled
Promise.allSettled 方法返回一个对象数组,每个对象代表参数中 promise 的状态和值,其中使用 status 表示状态,若参数中的 promise 的最终状态是 fulfilled,则使用 value 表示该 promise 的 promiseresult,否则使用 reason 表示。
我们可以使用一个 demo 来尝试使用一下 Promise.allSettled 方法:
let p1 = new Promise((resolve, reject) => {
resolve('1')
})
let p2 = new Promise((resolve, reject) => {
reject('2')
})
let p3 = new Promise((resolve, reject) => {
resolve(new Promise((resolve, reject) => {
setTimeout(function () {
resolve('3')
})
}))
})
let p4 = new Promise((resolve, reject) => {
reject(new Promise((resolve, reject) => {
setTimeout(function () {
resolve('3')
})
}))
})
let p5 = {
then: (resolve, reject) => {
resolve({
then: (resolve, reject) => {
resolve('5')
}
})
}
}
let p6 = {
then: (resolve, reject) => {
reject({
then: (resolve, reject) => {
resolve('5')
}
})
}
}
let result = Promise.allSettled([p1, p2, p3, p4, p5, p6])
console.log(result)//promise:{promiseStatus:pending}
setTimeout(function () {
console.log(result)
})
/*promise:{promiseStatus:'fulfilled',promiseResult:[
[
{
"status": "fulfilled",
"value": "1"
},
{
"status": "rejected",
"reason": "2"
},
{
"status": "fulfilled",
"value": "3"
},
{
"status": "rejected",
"reason": promise:{promiseStatus:'fulfilled',promiseResult:3}
},
{
"status": "fulfilled",
"value": "5"
},
{
"status": "rejected",
"reason": {then:function(resolve,reject){
resolve('5')
}}
}
]}*/
再看另外一个 demo:
let p=Promise.allSettled([])
console.log(p)//promise:{promiseStatus:'fulfilled',promiseResult:[]}
结合这两个 demo 可以看出,Promise.allSettled 和 Promise.all 两个方法有许多共同点:
- 当参数迭代器为空,同步返回状态为
fulfilled,值为[]的Promise实例。 - 当参数迭代器中包含嵌套
promise或者嵌套thenable对象的时候,若状态是fulfilled,处理的时候会展开,换句话说,会递归拿到最终值;若状态是rejected,则会直接返回。 不同点: - 当
Promise.all方法遇到rejected状态的时候,则直接修改结果的Promise实例状态为rejected,原因和在参数中首次遇到的rejected状态的原因保持一致; - 当
Promise.allSettled方法遇到rejected状态的Promise实例时,只是单纯的记录下来,等所有的迭代器中的Promise状态更改完毕后,返回记录;
接下来我们可以尝试写写 Promise.allSettled 的源码:
Promise.allSettled = function (params) {
if (!(typeof params[Symbol.iterator] === 'function')) {
throw new TypeError('params is not an iterator')
}
const result = [];
let count = 0;
for (let i = 0; i < params.length; i++) {
const item = params[i];
//Promise.resolve 处理迭代器中的非promise 对象,把它们变成 promise
//then 方法拿到 promise 的 result,并且可以展开循环嵌套
Promise.resolve(item).then((result) => {
result[i] = { status: 'fulfilled', value: result };
if (++count === params.length) {
resolve(result);
}
}, (reason) => {
++count;
result[i] = { status: 'rejected', reason };
});
}
}
Promise.race
race 翻译过来就是比赛的意思,先到先返回。在接收的一组 promise 中,根据最快的一组决定返回的 promise 的状态和结果。看看下面的 demo:
let p= Promise.race([new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve(1)
})
}),new Promise((resolve,reject)=>{
resolve(2)
})])
setTimeout(()=>{
console.log(p)//promise:{promiseStatus:fulfilled,promiseResul:2}
})
我们可以尝试手写 Promise.race 的源码:
Promise.race = function (params) {
return new Promise((resolve, reject) => {
for (let i = 0; i < params.length; i++) {
const item = params[i];
Promise.resolve(item).then(
(value) => {
resolve(value)
}, (reason) => {
reject(reason)
})
}
})
}
和上面两个方法类似,我们使用 Promise.resolve 将参数迭代器中非 promise 的参数变成 promise 统一处理。根据手写源码我们可以知道,then 方法帮我们做了两件事:
- 暴露出了当前
Promise实例的result/reason - 展平了嵌套的
Promise实例
Promise.any
Promise.any 方法和 Promise.all 方法是相反的:
all方法是全对返回所有结果,错一个返回错误的any方法是全错返回错误的,对一个返回正确的 于是我们可以在Promise.all的手写源码基础上进行修改,得到下面代码:
Promise.any = function (params) {
if (!(typeof params[Symbol.iterator] === 'function')) {
throw new TypeError('params is not an iterator')
}
return new Promise((resolve, reject) => {
//参数为空的时候,直接返回状态为 fulfilled 的 promise,result 为[]
const final = []
let len = args.length;
if (len === 0) return resolve(final);
for (let i = 0; i < params.length; i++) {
const item = params[i];
//遇到 resolve,直接改变结果 promise 的 promiseresult 和 promisestatus,后续的更改不会生效
Promise.resolve(item).then((result) => {
resolve(result);
}, (reason) => {
// 一旦有promise被拒绝就放入数组,攒满后才会返回
final[i] = reason;
if (--len === 0) {
reject(final);
}
});
}
});
};
错误处理 ❌
catch 和 finally 方法定义在 Promise 的原型上,可以被 Promise 实例继承,在 promise 的链式调用中十分重要。
Promise.prototype.catch
有一个经常被提及的概念就是异常传透。
什么是异常传透?让我们通过 demo 来理解一下:
new Promise((resolve, reject) => {
reject('我失败了~')
}).then(
(value) => { console.log('onfulfilled1', value) },
).then(
(value) => { console.log('onfulfilled2', value) },
).then(
(value) => { console.log('onfulfilled3', value) },
).catch(
(err) => { console.log('onRejected1', err) },//我失败了
)
异常传透说的是在写 promise 链式调用的时候,如果我们不主动写 onRejected 方法去捕获错误的话,错误会一直往下传递,直到被 catch 捕获,在学习如何手写 Promise 源码之后,我们可以知道,在 then 方法中,当 onRejected 方法为 undefined 的时候,会默认给给一个异常抛出的方法:
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason; };
换句话说,我们表面上看到最后一个 catch 拿到了第一个 promise 的错误信息,实际上错误信息还是通过第一个 promise 逐个的往下进行的传递,于是我们可以想到,实际上这个 catch 方法等效于下面这种写法:
new Promise((resolve, reject) => {
reject('我失败了~')
}).then(
(value) => { console.log('onfulfilled1', value) },
).then(
(value) => { console.log('onfulfilled2', value) },
).then(
(value) => { console.log('onfulfilled3', value) },
).then(
(value)=>{ console.log('onfulfilled4', value)},
(err) => { console.log('onRejected1', err) },//我失败了
)
所以我们可以尝试写一下 catch 方法的源码:
class Promise {
...
catch (onRejected) {
return this.then(null, onRejected);
}
...
}
Promise.prototype.finally
finnally 方法返回一个 Promise 实例。在链式调用结束的时候,无论结果如何,都会执行这个函数。
我们可以通过一个 demo 来看看 finally 的基本使用:
let presolve = Promise.resolve('success')
let preject = Promise.reject('fail')
let p1 = presolve.finally(() => { })
let p2 = preject.finally(() => { })
setTimeout(function () {
console.log(p1)//promise:{promiseStatus:fulfilled,promiseResult:'success'}
console.log(p2)//promise:{promiseStatus:rejected,promiseResult:'fail'}
console.log(p1 === presolve)//false
console.log(p2 === preject)//false
})
从这个 demo 中我们可以看到 finally 方法的基本用法,除此之外还有两个小细节:
finlly的返回值和调用finally方法的Promise实例的promiseStatus和promiseResult需要保持一致。finlly的返回值和调用finally方法的Promise实例不是同一个对象,返回的是新的Promise实例。 于是我们可以尝试写一下Promise.prototype.finally的源码:
class Promise {
// ...
finally(onFinally) {
return new Promise((resolve, reject) => {
this.then((result) => {
onFinally();
resolve(result);
}, (reason) => {
onFinally();
reject(reason);
});
});
}
// ...
}
兼容性查询 ❓
以上所以的 Promise 的静态方法和错误处理 api 的兼容性可以通过下面这个网站进行查询:
小声 BB 🤡
又到了我最喜欢的小声BB环节,新年 🎏 flag 🎏 在持续稳步的推进中.
深圳疫情又严重了,前几天看到科兴的同行提着台式机跑毒差点笑出了声,笑完突然想到前几天猝死的吴姓同行,不禁又开始思考程序员这个岗位,转而到思考人生意义,但是仍旧没有思考出个所以然。
年前到做了一次年终体检,前几天到拿体检报告:
尿酸,血糖,血脂全部告警,可能还没等到被社会优化,自己就先把自己给优化了。
最近,在低调青年群也接触到了正念冥想,下载了一个相关 APP,开始尝试一下。
读书方面,在读鲁迅先生的《呐喊》,发现很多网友们玩的不亦乐乎的梗,加上历史背景的 buff 之后,其代表的意义竟是如此的沉重。
三月想继续把这个系列写一下,同时还想分享一下最近体验低代码平台的一些感受,还想分享一下学习 sketch 的收获,要分享的还有很多,to be continue。。。
🎉 🎉 觉得文章对您有帮助的小伙伴,请不要吝啬您的点赞~🎉 🎉
🎉 🎉 对文章中的措辞表达、知识点、文章格式等方面有任何疑问或者建议,请留下您的评论~🎉 🎉