前言
一直以为自己已经掌握了promise,但最近摸🐟想自己手写一个promise的时候,连实现链式调用都写不出来,加上前阵子去面试的时候,被问到promise的频率非常高,等到自己真正被问懵的时候才知道自己对promise的理解并不够透彻,因此想到写下这篇文章,让自己对Promise
有更深刻的理解👍
在这里声明一下,本人从入坑前端以来系统知识的学习都是看coderwhy老师的课程,因此本篇文章内容主要来自coderwhy老师的javascript高级教学视频加上一些小的知识点补充,再用自己的话表达出来,有什么不正确的地方望大佬指正👣
一、Peomise解决的痛点
鲁迅先生曾曰:
'新技术的出现必定是为了解决旧技术的某些痛点'
。
我们都知道,在Promise出现之前,我们对异步任务的处理,一般是通过回调来实现。以下面这段代码为例,我们封装了一个函数,用定时器⏰来模拟异步任务,我们希望在某些条件成立的时候执行一些成功
或者失败
的操作,ES6 之前,处理异步函数的代码都是这样包装的
function asyncCode(couter, successCallback, failureCallback) {
setTimeout(() => {
if (counter > 0) {
let total = 0
for (let i = 0; i < counter; i++) {
total += i
}
successCallback(total)
} else {
failureCallback(`${counter}不符合`)
}
}, 300)
}
asyncCode(10, (total) => {
console.log(total)
}, (errmsg) => {
console.log(errmsg)
}) //45
在上面这个例子中,我们确实可以解决通过请求函数得到结果之后执行相应的回调函数,得到想要的结果,但这种方法会存在以下几种问题
- 第一,我们需要自己设计回调函数,回调函数的名称,回调函数的使用
- 第二,基于第一点,每个人设计出来的回调方案都是不同的,我们需要耐心看别人的源码或者文档,才可以理解这个函数是怎么使用的,耗时耗精力
- 第三,当我们异步任务(比如异步网络请求任务)非常多的时候,会在一个函数里嵌套很多个回调函数,形成回调地狱,造成代码可读性和维护性很差👎
🥳Promise的出现解决了这些问题,下面我们开始正式进入Promise的学习伐
二、什么是Promise
promise概念
mdn对Promise的解释是是一个对象,用来表示一个异步操作的最终完成(或失败)及其结果值
,它能够把异步操作最终的成功返回值或者失败原因和相应的处理程序关联起来,使得异步方法可以像同步方法那样有返回值,异步方法不会立即返回最终的值,而是会返回一个 promise,以便在未来某个时候把值交给使用者
这段解释可能有点抽象官方,其实就是当我们的异步操作执行完成后,这时候会给予调用者一个承诺:待会我会给你回调数据,你可以拿到这个数据再去做其他操作,以此创建出一个Promise
对象
promise状态详解
一个Promise必然会处于以下三个状态之一:
pending
:初始状态,既没有被兑现,也没有被拒绝;fullfilled
:意味着操作成功完成;rejected
:意味着操作失败;
我们通过代码解释一下这三种状态在代码中的体现
- 以下代码是通过
new
创建一个Promise
对象,我们需要传入一个回调函数,文章后续会称这个回调函数为executor
,resolve
和reject
是executor
接收的两个回调函数
let p1 = new Promise((resolve, reject) => {
console.log("myPromise")
})
console.log(p1)
从这段代码我们可以看到executor
函数体只打印了一句话,我们执行这段代码,结果如下,可以看到executor
里面的代码被立即执行,同时p1处于初始状态pending
- 接着我们在
executor
里面调用resolve回调函数
let p1 = new Promise((resolve, reject) => {
console.log("myPromise")
resolve()
})
console.log(p1)
在控制台查看打印结果如下吗,可以看到,此时p1的状态从pending
变成fullfilled
- 接着我们注释掉resolve,改为调用reject回调函数
let p1 = new Promise((resolve, reject) => {
console.log("myPromise")
reject()
})
console.log(p1)
在控制台查看打印结果如下吗,可以看到,此时p1的状态从pending
变成rejected
通过以上代码演示,可以知道当我们的executor
没有调用任何回调函数之前,promise
会处于pending
状态,调用resolve
之后状态变为fullfilled
,即异步操作成功,调用reject
之后状态变为rejected
,也就是异步操作失败
🧐这时有人会问了,那如果我同时调用resolve
和rejected
呢,满足小朋友的好奇心,我们以代码演示一下👎
可以看到,当我们在executor
同时调用两个回调函数时,不管哪个先调用,都只有一个会生效,从这个示例也可以得出一个结论,promise
的状态只能从pending
=>fullfilled
或者从pending
=>rejected
总结
Executor
是在创建Promise
时需要传入的一个回调函数,这个回调函数会被立即执行,并且传入两个参数,通常我们会在Executor
中确定我们的Promise
状态,通过resolve
,可以兑现(fulfilled
)Promise
的状态,我们也可以称之为已决议,通过reject
,可以拒绝(reject
)Promise
的状态。
一旦状态被确定下来,Promise
的状态会被锁死,该Promise
的状态是不可更改的,在我们调用resolve
的时候,如果resolve
传入的值本身不是一个Promise
(resolve
传参的区别后续会讲到),那么会将该Promise
的状态变成 兑现(fulfilled
),在之后我们去调用reject
时,已经不会有任何的响应了
三、Promise的使用
前文讲了那么多,到头来也没有说到promise
怎么使用,状态改变之后发生什么,怎么把异步操作的结果与异步之后的程序相关联起来,别急😁,这一章会详细的解释怎么使用Promise
执行resolve或reject之后发生了什么
这里我们直接说下答案:当我们调用resolve
回调函数时,会执行Promise
对象的then方法传入的回调函数;当我们调用reject
回调函数时,会执行promise
对象的catch
方法传入的回调函数。
then
方法和catch
方法是Promise
原型上的一个方法:then
方法会返回一个Promise
,最多可以接收两个参数,分别是Promise
成功和失败情况的回调函数;catch
方法也返回一个Promise
,当我们的Promise
被拒绝时(reject
)调用catch
里的回调函数。
下面通过代码演示一下then
方法和catch
方法,注释掉reject()
,打印的是‘失败的回调’,注释掉resolve()
,打印的是‘成功的回调’
let p1 = new Promise((resolve, reject) => {
reject()
// resolve()
})
console.log(p1)
p1.then(() => {
console.log('成功的回调')
}, () => {
console.log("失败的回调")
})
以上代码等价于
let p1 = new Promise((resolve, reject) => {
reject()
// resolve()
})
console.log(p1)
p1.then(() => {
console.log('成功的回调')
}).catch(() => {
console.log("失败的回调")
})
resolve回调函数参数的处理
- 情况一:如果
resolve
传入一个普通的值或者对象,那么这个值会作为then
回调的参数
let p1 = new Promise((resolve, reject) => {
resolve('情况一:普通值或对象')
})
p1.then((res) => {
console.log(res) //情况一:普通值或对象
})
let p1 = new Promise((resolve, reject) => {
resolve({obj:'情况一:普通值或对象'})
})
p1.then((res) => {
console.log(res) //{obj: '情况一:普通值或对象'}
})
- 情况二:如果
resolve
中传入的是另外一个Promise
,那么这个新Promise
会决定原Promise
的状态,并且新Promise
的resolve
或reject
的参数也会传给原来的promise
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('我是新的promise')
}, 1000)
})
const p1 = new Promise((resolve, reject) => {
resolve(p)
})
p1.then((res) => {
console.log(res)
})
在这段代码中,p是p1resolve
中传入的Promise
,因此,p1的状态是由p的状态决定的,由于p执行了resolve
回调函数,因此状态是fullfilled
,所以p1的状态也是fullfilled
,运行之后控制台在1s后打印出“我是新的promise”
const p = new Promise((resolve, reject) => {
setTimeout(() => {
reject('我是新的promise并且拒绝')
}, 1000)
})
const p1 = new Promise((resolve, reject) => {
resolve(p)
})
p1.then((res) => {
console.log(”res:“+res)
}).catch((err) => {
console.log(“err:”+err)
})
修改p回调的函数为reject之后,控制台打印“err:我是新的promise并且拒绝”,进一步验证以上结论
- 情况三:resolve传入的是一个对象,并且该对象有实现then方法,那么会执行这个then方法,并且根据then方法的结果来决定Promise的状态
const p = {
name: 'javascript',
then: function (resolve) {
resolve("情况三")
}
}
const p1 = new Promise((resolve, reject) => {
resolve(p)
})
p1.then((res) => {
console.log("res:" + res)
}).catch((err) => {
console.log("err:" + err)
})
以上代码在控制台输出“res:情况三”
Promise的.then和.catch方法的调度
我们前面对then和catch方法的调用都只有一次,假如我们调用了多次呢🤔
const p1 = new Promise((resolve, reject) => {
resolve("成功的回调")
})
p1.then((res) => {
console.log("res1:" + res)
})
p1.then((res) => {
console.log("res2:" + res)
})
p1.then((res) => {
console.log("res3:" + res)
})
打开控制台会发现每个then里面的回调都被执行了,说明then是可以被多次调用的,每次调用我们都可以传入对应的fulfilled
回调,当Promise
的状态变成fulfilled
的时候,这些回调函数都会被执行。
那么catch
是否也会出现相同的结果呢,当我们在执行下面这段代码的时候,控制台会打印所有catch
回调里面的内容,因此catch
也是可以被多次调用的,每次调用我们都可以传入对应的reject
回调;当Promise
的状态变成reject
的时候,这些回调函数都会被执行;
const promise = new Promise((resolve, reject) => {
reject("failure")
})
promise.then(res => {
console.log("成功的回调:", res)
}).catch(err => {
console.log("失败的回调1:", err)
})
promise.catch(err => {
console.log("失败的回调2:", err)
})
promise.catch(err => {
console.log("失败的回调3:", err)
})
promise.catch(err => {
console.log("失败的回调4:", err)
})
promise.catch(err => {
console.log("失败的回调5:", err)
})
then和catch方法的返回值
1、then
then
方法本身是有返回值的,他的返回值是一个Promise
,所以我们可以对其进行链式调用
const p1 = new Promise((resolve, reject) => {
resolve("成功的回调")
})
p1.then((res) => {
console.log("res:" + res)
return res + 'abc'
}).then((res1) => {
console.log("res1:" + res1)
})
控制台会输出 res:成功的回调 res1:成功的回调abc
那么.then
方法返回的这个Promise
的状态是由什么决定的呢,事实上,这个新Promise
的决议是等到then
方法传入的回调函数有返回值时, 才会进行决议。
当then
方法中的回调函数在执行的时候,返回的promise
处于pending
状态,当返回一个结果时,会处于fullfilled
状态,并且将结果作为resolve
的参数,因此链式调用的then
方法里的回调函数的参数是上一个then
方法的返回值
下面我们来谈谈then方法返回值的几种情况吧😁
- 情况一:返回一个普通的值
a
。将这个值a
作为resolve
的参数,因此在后面的.then方法里的回调函数获取到的参数就是a
const p1 = new Promise((resolve, reject) => {
resolve("成功的回调")
})
p1.then((res) => {
return 'aaaaa'
}).then((res1) => {
console.log("res1:" + res1) //res1:aaaaa
return 'bbbbb'
}).then((res2) => {
console.log("res2:" + res2) //res2:bbbbb
})
- 情况二:返回一个
Promise
。如果返回了一个PromiseA
,那么then
返回的PromiseB
的状态会由PromiseA
的状态决定,并且将PromiseA
的状态的回调函数的参数作为PromiseB
的状态的回调函数的参数
const p = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('ccccc')
}, 1000)
})
const p1 = new Promise((resolve, reject) => {
resolve("成功的回调")
})
p1.then((res) => {
return p
}).then((res1) => {
console.log("res1:" + res1)
return 'ddddd'
}).then((res2) => {
console.log("res2:" + res2)
})
上面这段代码在1s后输出res1:ccccc
,res2:ddddd
- 情况三:返回一个
thenable
对象,如果then方法里面的回调函数返回了一个带有then方法的对象,那么then方法返回的PromiseAd的状态是由then方法里的结果决定的
const promise = new Promise((resolve, reject) => {
resolve("aaaaaaa")
// reject()
})
promise.then(res => {
return {
then: function (resolve, reject) {
resolve("thenable")
}
}
}).then(res => {
console.log("thenable test:", res) // thenable test: thenable
})
2、catch
catch也会返回一个Promise,因此也是支持链式调用的,且catch后面可以调用then或者catch方法,我们先来看下面一段代码
const p1 = new Promise((resolve, reject) => {
reject("失败的回调")
})
p1.catch((res) => {
console.log('catch里面的回调函数')
}).then((res1) => {
console.log("res1:" + res1)
}).catch((res2) => {
console.log("res2:" + res2)
})
大家伙猜猜这段代码会打印出什么,是catch里面的回调函数 res2:undefined
呢,还是catch里面的回调函数 res1:undefined
,下面为大家揭晓答案
事实上,在p1.catch
执行完后是会执行.then
后面的回调而不是.catch
后面的回调,这是因为为catch
传入的回调在执行完后,默认状态依然会是fulfilled
的;
如果我们想让.catch后面继续执行catch该怎么做呢❓答案是我们需要抛出一个异常,如下面代码所示
const p1 = new Promise((resolve, reject) => {
reject("失败的回调")
})
p1.catch((res) => {
console.log('catch里面的回调函数')
throw new Error('error message')
}).then((res1) => {
console.log("res1:" + res1)
}).catch((res2) => {
console.log("res2:" + res2)
})
控制台会输出
综上:链式调用中的.catch方法的执行时机,是由上一个promise是否抛出异常决定的,如果上一个Promise照常返回一个值,执行的是链式调用中的then方法
Promise的finally
finally是ES9中新增的一个特性,无论promise变成fullfilled状态还是rejected状态,都会执行finally里面的回调,而且finally不接收任何参数,也用代码演示一遍
let p1 = new Promise((resolve, reject) => {
resolve('abc')
})
p1.then((res) => {
console.log(res)
}).catch((err) => {
console.log(err)
}).finally(() => {
console.log('finally')
})
上面代码的执行结果是abc finally
四、promise的类方法
前面我们讲过的方法都是属于Promise
的实例方法,存放在Promise
的prototype
上,实际上,Promise
也存在类方法,且在实际开发中用到的频率也很高,下面我们以此介绍一下Promise
最主要的几个类方法
Promise.all
Promise.all
的作用是将多个Promise
包裹在一起形成一个新的Promise
,并且这个新的Promise
的状态是由包裹的Promise
的状态共同决定的:
- 当所有的
Promise
的状态变成fullfilled
,新的Promise
的状态变为fullfilled
,并将所有promise
的返回值组成一个数组 - 当有一个
Promise
的状态变成reject
,新的Promise
的状态会变成reject
,并且会将第一个reject
的Promise
的返回值作为参数
let p1 = new Promise((resolve, reject) => {
resolve('Promise')
})
let p2 = p1.then((res) => {
return res + 'aaa'
})
let p3 = p2.then((res) => {
return res + 'bbb'
})
Promise.all([p1, p2, p3]).then(res => {
console.log(res) // ["Promise", "Promiseaaa", "Promiseaaabbb"]
})
let p1 = new Promise((resolve, reject) => {
resolve('Promise')
})
let p2 = new Promise((resolve, reject) => {
reject('reject状态的Promise')
})
Promise.all([p1, p2]).then((res) => {
console.log('res:' + res)
}).catch(err => {
console.log('err:' + err) //err:reject状态的Promise
})
Promise.resolve
Promise.resolve(res)
方法返回一个以给定值解析后的 Promise
对象,有时候我们已经有一个现成的值希望将其转换成Promise
可以使用该类方法
Promise.resolve('这是一个Promise')
//等价于
new Promise(resolve=>resolve('这是一个Promise'))
其中resolve
方法中的参数的类型,同第三章中回调函数参数的处理
一致。如果这个值是一个 promise
,那么将返回这个 promise
;如果这个值是 thenable
(即带有 then
方法),返回的 promise
会“跟随”这个 thenable
的对象,采用它的最终状态;否则返回的 promise
将以此值完成
Promise.reject
reject
方法类似于resolve
方法,只是会将Promise
对象的状态设置为reject
状态
Promise.reject('这是一个Promise')
//等价于
new Promise((resolve,reject)=>reject('这是一个Promise'))
Promise.reject
传入的参数无论是什么形态,都会直接作为reject
状态的参数传递到catch
的
Promise.race
如果有一个Promise
有了结果,我们就希望决定最终新Promise
的状态,那么可以使用race
方法,如以下代码输出结果为‘err:Promise1’
let p1 = new Promise((resolve, reject) => {
reject('Promise1')
})
let p2 = new Promise((resolve, reject) => {
reject('Promise2')
})
Promise.race([p1, p2]).then((res) => {
console.log('res:' + res)
}).catch(err => {
console.log('err:' + err) //err:Promise1
})
Promise.any
any
方法是ES12中新增的方法,和race
方法是类似的,不同的是any
方法会等到一个fulfilled
状态,才会决定新Promise
的状态,如果所有的Promise
都是reject
的,那么也会等到所有的Promise
都变成rejected
状态。
如果所有的
Promise
都是reject
的,那么会报一个AggregateError
的错误
let p1 = new Promise((resolve, reject) => {
reject('Promise1')
})
let p2 = new Promise((resolve, reject) => {
reject('Promise2')
})
Promise.any([p1, p2]).then((res) => {
console.log('res:' + res)
}).catch(err => {
console.log(err) //AggregateError: All promises were rejected
})
Promise.allSettled
all
方法有一个缺陷:当有其中一个Promise
变成reject
状态时,新Promise
就会立即变成对应的reject
状态。那么对于resolved
的,以及依然处于pending
状态的Promise
,我们是获取不到对应的结果的
在ES11(ES2020)中,添加了新的API Promise.allSettled
,该方法会在所有的Promise
都有结果(settled
),无论是fulfilled
,还是rejected
时,才会有最终的状态
并且这个Promise
的结果一定是fulfilled
的,以代码演示一下
let p1 = new Promise((resolve, reject) => {
reject('Promise1')
})
let p2 = new Promise((resolve, reject) => {
reject('Promise2')
})
let p3 = new Promise((resolve, reject) => {
resolve('Promise3')
})
Promise.allSettled([p1, p2, p3]).then((res) => {
console.log(res)
}).catch(err => {
console.log(err)
})
可以看出allSettled
的结果是一个数组,数组中存放着每一个Promise
的结果,并且是对应一个对象的;这个对象中包含status
状态,以及对应的value
值
总结:
Promise
是每个前端都需要弄清楚的一个知识点,在我们实际开发项目中也是大有用处的。本人以前初学的时候总以为自己学会了,但后面回顾的时候又总觉得自己没有学过这个东西一样,根本没有真正理解Promise
的用处,最近在手写一个Promise
的时候思路被卡住了,于是决定写一篇关于Promise
的文章,让知识点刻在脑子里,再去实现Promise
,边学边记边理解,效果比只看而不写好得多
这篇文章是本人的第一篇文章,本人也只是一个每天在公司写业务的小前端,肯定有很多不足的地方,有误的地方望大家批评指正,也是我继续写文章的动力👣
作者:https://juejin.cn/post/7240859916403015739