常见Promise面试题
我们看一些Promise的场景面试问法,由浅入深
- 1、了解Promise吗?
- 2、Promise解决的痛点是什么?
- 3、Promise解决的痛点还有其他方法可以解决吗?如果有,请举例
- 4、Promise如何使用?
- 5、promise常用的方法有哪些?他们的作用是什么?
- 6、Promise在事件循环中的执行过程是怎样的
- 7、Promise的业界实现都有哪些
- 8、能不能手写一个Promise的polyfill。
Promise出现的原因
在Promise出现之前,我们处理一个异步网络请求,大概是这样子
ajax({
url:请求地址
success:function(data){ //成功时的回调
//处理请求结果
}
})
看起来还不错 但是,需求变化了,我们需要根据第一个网络请求的得到结果,再去执行第二个网络请求,代码大概如下
ajax({
url: 请求的地址
success: function(data){ //成功时的回调
ajax({
url: 请求的地址
success:function(data){
处理请求结果2
}
})
}
})
看起来好像也不复杂 但是,需求是永无止境的,于是乎出现了下面的代码
ajax({
url:请求的地址
success: function(data){
ajax({
url:请求的地址
success:function(data){
ajax({
url:请求的地址
success:function(data){
ajax({
url:请求的地址
success:function(data){
ajax({
url:请求的地址
success:function(data){
ajax({
url:请求的地址
success: function(data){
...
}
})
}
})
}
})
}
})
}
})
}
})
这回傻眼了吧。。。 臭名昭著的 回调地狱 现身了。
回调地狱:某个异步操作需要等待之前的异步操作完成,无论用回调还是事件,都会陷入不断的嵌套
然而更糟糕的是,我们基本还要对每次请求的结果进行一些处理,导致代码会更加臃肿,在一个团队中,代码review以及后续的维护将会是一个很痛苦的过程。
回调地狱带来的负面作用有以下几点:
- 代码臃肿
- 可读性差
- 耦合度过高,可维护性差
- 代码复用性差
- 容易滋生bug
- 只能在回调里处理异常
为了更加深刻的体验到回调地狱,我们来看一个例子:
案例1:获取李华所在班级的老师的信息
1. 获取李华的班级id
2. 根据班级id获取李华所在班级的老师
3. 根据老师的id查询老师信息
//1、首先获取李华所在班级id
ajax({
url:"./data/students.json",
success: function(data){
for(let i = 0; i < data.length; i++){
if(data[i].name === "李华"){
const cid = data[i].classId;
//2、根据班级id获取李华所在班级的老师的id
ajax({
url:"./data/classes.json",
success:function(data){
for(let i = 0; i < data.length; i++){
if (data[i].id === cid) {
const tid = data[i].teacherId;
// 3. 根据老师的id查询老师信息
ajax({
url: "./data/teachers.json?id=" + tid,
success: function(data) {
for (let i = 0; i < data.length; i++) {
if (data[i].id === tid) {
console.log(data[i]);
}
}
}
})
return;
}
};
}
})
return;
}
}
}
})
上面仅仅嵌套了三层回调,代码看起来就已经如此恶心。根本提不起让人提起阅读的欲望。
因此,出现了问题,自然就会有人想去想办法解决。这是,社区里就有人思考了,能不能用一种更加有好的代码组织方式,解决异步嵌套的问题
let 请求结果1 = 请求1();
let 请求结果2 = 请求2(请求结果1);
let 请求结果3 = 请求3(请求结果2);
let 请求结果4 = 请求2(请求结果3);
let 请求结果5 = 请求3(请求结果4);
类似上面这种同步的写法,于是Promise规范诞生了,并且在业界有了很多实现来解决回调地狱的痛点,比如业界著名的Q和号称运行最快的类库bluebird
看官们看到这里,对于上面的问题 2 和问题 7 ,心中是否有了答案呢。^_^
答案2:Promise解决了回调地狱的嵌套问题,但是并没有消除回调,回调依然存在,Promise只是让回调变得可控
什么是Promise
ES官方参考了大量的异步场景,总结出了一套异步的通用模型,该模型可以覆盖几乎所有的异步场景,甚至是同步场景。
值得注意的是,为了兼容旧系统,ES6 并不打算抛弃掉过去的做法,只是基于该模型推出一个全新的 API,使用该API,会让异步处理更加的简洁优雅。
理解该 API,最重要的,是理解它的异步模型

- ES6 将某一件可能发生异步操作的事情,分为两个阶段:unsettled 和 settled
- unsettled: 未决阶段,表示事情还在进行前期的处理,并没有发生通向结果的那件事
- settled:已决阶段,事情已经有了一个结果,不管这个结果是好是坏,整件事情无法逆转
并且,事情总是从 未决阶段 逐步发展到 已决阶段的。并且,未决阶段拥有控制何时通向已决阶段的能力。
- ES6将事情划分为三种状态:pending,resolved,rejected
- pending: 挂起,处于未决阶段,则表示这件事情还在挂起(最终的结果还没出来)
- resolved:已出来,已决阶段的一种状态,表示整件事情已经出现结果,并且是一个可以按照正常逻辑进行下去的结果
- rejected:已拒绝,已决阶段的一种状态,表示整件事情已经出现结果,并是一个无法安装正常逻辑进行下去的结果,通常用于表示有一个错误
既然未决阶段有权利决定事情的走向,因此,未决阶段可以决定事情最终的状态!
我们将 把事情变成resolved状态的过程叫做:resolve,推向该状态时,可能会传递一些数据
我们将 把事情变成rejected状态的过程叫做:reject,推向该状态时,同样可能传递一些数据,通常为错误信息 始终记住,无论是阶段,还是状态,是不可逆的!
- 当事情达到已决阶段后,通常需要进行后续处理,不同的已决状态,决定了不同的后续处理。
- resolved状态:这是一个正常的已决状态,后续处理表示为thenable
- rejected状态:这是一个非正常的已决状态,后续处理表示为catchable 后续处理可能有多个,因此会形成作业队列,这些后续处理会按照顺序,当状态到达后依次执行
- 至此, 整件事称之为Promise
const pro = new Promise((resolve, reject)=>{
// 未决阶段的处理
// 通过调用resolve函数将Promise推向已决阶段的resolved状态
// 通过调用reject函数将Promise推向已决阶段的rejected状态
// resolve和reject均可以传递最多一个参数,表示推向状态的数据
})
pro.then(data=>{
//这是thenable函数,如果当前的Promise已经是resolved状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达resolved状态后执行
//data为状态数据
}, err=>{
//这是catchable函数,如果当前的Promise已经是rejected状态,该函数会立即执行
//如果当前是未决阶段,则会加入到作业队列,等待到达rejected状态后执行
//err为状态数据
})
细节
- 未决阶段的处理函数是同步的,会立即执行
- thenable 和catchable 函数是异步的,就算是立即执行,也会加入到事件队列中等待执行,并且加入的是微队列
- pro.then可以只添加thenable函数,pro.catch可以单独添加catchable函数
- 在未决阶段的处理函数中,如果发生未捕获的错误,会将状态推向rejected,并会被catchable捕获
- 一旦状态推向的已决阶段,无法在对状态做任何更改
- Promise并没有消除回调,只是让回调变得可控
Promise的串联
什么是Promise的串联?就是当后续的Promise需要用到之前的Promise的处理结果时,就需要用到Promise的串联
Promise对象中,无论是then方法还是catch方法,它们都具有返回值,返回的是一个全新的Promise对象,它的状态满足下面的规则:
- 如果当前的Promise是未决的,得到的新的Promise是挂起状态
- 如果当前的Promise是已决的,会运行响应的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中;如果后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中。
后续的Promise一定会等到前面的Promise有了后续处理结果后,才会变成已决状态
如果前面的Promise的后续处理,返回的是一个Promise,则返回的新的Promise状态和后续处理返回的Promise状态保持一致。
了解了Promise后,再用Promise实现前面案例一
案例:获取李华所在班级的老师的信息(前提需要将ajax异步请求变成Promise对象)
查看案例:codesandbox.io/s/dazzling-…
//1. 获取李华的班级id Promise
//2. 根据班级id获取李华所在班级的老师id Promise
//3. 根据老师的id查询老师信息 Promise
ajax({
url:"./data/student.json";
}).then(resp=>{ //then(1) 辅助说明作用
for(let i = 0; i < resp.length; i++){
if(resp[i].name === "李华"){
return resp[i],classId;
}
}
},err=>{
console.log(err);
}).then(cid=>{ //then(2) 辅助说明作用
return ajax({
url:"./data/classes.json"
}).then(cls =>{
for(let i = 0; i< cls.length; i++){
if(cls[i).id === cid{
return cls[i].teacherId;
}
}
})
}).then(tid=>{ //then3 辅助说明作用
return ajax({
url: "./data/teachers.json"
}).then(tls=>{
for(let i = 0; i < tls.length; i++){
if(tls[i).id = tid{
return tls[i]
}
}
})
}).then(info=>{ //then4
console.log(info);
})
运行流程解析:首先,调用ajax请求获取所有学生信息,返回一个Promise对象,如果该Promise对象是已决阶段的resolved状态,调用该Promise对象的then(1)方法时,会立即执行里面的后续处理函数,然后得到一个包含所有学生信息json数据,循环得到的学生数据,返回名字为李华的学生所在的班级id。然后顺序,我们接下来会发送第二次的ajax请求获取班级数据,但这时,我们发现,我们需要用到上一个Promise的then1方法的后续处理函数运行返回的班级id结果,当成第二次ajax请求获取到的所有班级数据中的条件,从而拿到对应班级的老师的id,这是我们就需要用到Promise的串联,根据Promise的串联规则,如果第一次ajax请求得到的Promise状态是未决的,then1得到的新的Promise是挂起状态,如果第一次ajax请求得到的Promise是已决的,会运行then1的后续处理函数,并将后续处理函数的结果(返回值)作为resolved状态数据,应用到新的Promise中(then2的Promise),如果then1后续处理函数发生错误,则把返回值作为rejected状态数据,应用到新的Promise中(then2的promise)。此时then2的后续处理函数的参数cid接受到了then1的后续处理函数返回的结果,then2的promise状态为resolved,所以立即运行then2的后续处理函数,发送第二次ajax请求,返回一个resolved状态的promise对象,调用该then方法,执行后续处理函数。在里面遍历resolved的状态数据,也就是存放所有班级数据的json数据,根据对应的班级id条件,拿到对应班级老师的id,并返回出去,由于then2的后续处理函数返回的是一个Promise对象(第二个ajax请求),则then2返回的新的Promise状态和then2后续处理函数返回的Promise状态保持一致(第二个ajax请求),并将第二次ajax请求的then的后续处理函数返回的结果作为then2返回的新Promise对象的resolved状态数据,then2的后续处理函数运行结束后,开始运行then3的后续处理函数,第三次发送ajax请求,再次返回一个包含resolved状态和数据的Promise对象,运行第三次ajax请求的后续处理函数,得到所有老师信息,根据第二次ajax请求返回的id,得到该id对应的老师信息,并返回给then3返回的新Promise,作为该Promise resolved的状态数据,最后,运行该Promise的then方法(then4),输出该老师的信息。至此,整个运行结束。
API
Promise的常用它PI如下:
原型成员(3)
- then 注册一个后续处理函数,当Promise为resolved状态时运行该函数
const pro = new Promise((resolve,reject)=>{
resolve(1) ////将状态推向已决,并传递resolved状态数据
})
pro.then(data=>{
//data接收resolved传递的状态数据
console.log(data); //1
})
- catch 注册一个后续处理函数,当Promise为reject状态时运行该函数
const pro = new Promise((resolve,reject)=>{
//reject和抛出错误都能将状态推向reject的已决状态
// reject(2) //将状态推向已决,并传递reject状态数据
throw new Error(2) //将状态推向已决,并传递reject状态数据
})
pro.then(data=>{
},err=>{ //then可接收两个后续处理函数,resolve和reject的
console.log(err); //Error: 2
}) //catch只能接受一个reject后续处理函数
pro.catch(err=>{
console.log(err); //Error: 2
})
- finally [ES2018]注册一个后续处理函数(无参),当Promise为已决时运行该函数
const pro = new Promise((resolve, reject) => {
//resolve(1);
reject(2);
})
pro.then(data => {
console.log(data); //1
}, err => {
console.log(err); //2
})
pro.finally(() => {
console.log("finally"); //无论是reject状态还是resolve状态,都会执行该后续处理函数
})
始终记住,从未决阶段到已决阶段是不可逆的,通向已决阶段的最终只能有一个状态,不是resolved就是rejected,两种不能共存
构造函数成员(4)
- resolved(数据):该方法返回一个resolved状态的Promise,传递的数据作为状态数据
- 特殊情况:如果传递的数据是Promise,则直接返回传递的Promise对象,可以理解为复制了该promise的指针
const pro1 = Promise.resolve(1);
console.log(pro1); //Promise {<resolved>: 1}
// 等效于
const pro2 = new Promise((resolve,reject)=>{
resolve(1)
})
pro2.then(data=>{
console.log(data); //1
})
console.log(pro2); //Promise {<resolved>: 1}
const pro1 = Promise.resolve(2);
console.log(pro1); //Promise {<resolved>: 1}
const pro3 = Promise.resolve(pro1);//如果传递的是一个Promise对象
console.log(pro3); //Promise {<resolved>: 1}
console.log(pro1===pro3); //true
- reject(数据):该方法返回一个rejected状态的Promise,传递的数据作为状态数据
const pro1 = Promise.reject(2);
console.log(pro1); //Promise {<rejected>: 2}
// 等效于
const pro3 = new Promise((resolve,reject)=>{
reject(2);
})
console.log(pro3); //Promise {<rejected>: 2}
- all(iterable):这个方法返回一个新的promise对象,该promise对象在iterable参数对象里所有的promise对象都成功的时候才触发成功,一旦有任何一个iterable里面的promise对象失败则立即出发该promise对象的失败。这个新的promise对象咋触发成功状态后,会把一个包含iterable里所有的promise返回值的数组作为成功的回调返回值,顺序跟iterable的顺序保持一致,如果这个新的promise对象触发了失败状态,它会把iterable里第一个触发失败的promise对象的错误信息作为它的失败错误信息。Promise.all方法常用与被用于处理多个promise对象的状态集合。 查看例子:codesandbox.io/s/static-el…
function getRandom(min,max){
return Math.floor(Math.random()*(max - min) + min); //随机生成一个min -max区间的随机数
}
const arr = [];
for (let i = 0; i < 10; i++) {
arr.push(new Promise((resolve,reject)=>{
setTimeout(() => {
if (Math.random() < 0.9) {
console.log(i,"完成");
resolve(i)
}else{
console.log(i,"失败");
reject(i);
}
}, getRandom(1000,5000));
}))
}
const pro = Promise.all(arr);
pro.then(datas=>{
console.log(datas);
})
pro.catch(err=>{
console.log(err);
})
console.log(arr);
//接收一个存放Promise的数组,当数组里边的所有Promise状态达到resolved后,返回一个resolve状态的Promised对象,
//并传递一个由所有promise状态数据组成的数组当做返回的promise的状态数据,如果里边只要有一个promise变成
//reject状态,则会把第一个触发失败的Promise对象作为他的失败错误信息
- race(iterable):当iterable参数里的任意一个子promise被成功或失败后,父promise马上也会用子promise的成功返回值或失败详情作为参数调用父promise绑定的相应句柄,并返回该promise对象
<script>
function getRandom(min,max){
return Math.floor(Math.random()*(max - min) + min); //随机生成一个min -max区间的随机数
}
const arr = [];
for (let i = 1; i <=10; i++) {
arr.push(new Promise((resolve,reject)=>{
setTimeout(() => {
if (Math.random() < 0.5) {
console.log(i,"完成");
resolve(i)
}else{
console.log(i,"失败");
reject(i);
}
}, getRandom(1000,5000));
}))
}
const pro = Promise.race(arr);
pro.then(data=>{
console.log(data,"是第一完成的");
console.log(pro);
})
pro.catch(err=>{
console.log(err,"是第一失败的");
console.log(pro);
})
console.log(arr);
</script>
async 和await
async 和 await 是 ES2016 新增两个关键字,它们借鉴了 ES2015 中生成器在实际开发中的应用,目的是简化 Promise api 的使用,并非是替代 Promise。
async
目的是简化在函数的返回值中对Promise的创建
async 用于修饰函数(无论是函数字面量还是函数表达式),放置在函数最开始的位置,被修饰函数的返回结果一定是 Promise 对象。
async function test(){
console.log(1);
return 2;
}
//等效于
function test(){
return new Promise((resolve, reject)=>{
console.log(1);
resolve(2);
})
}
await
await关键字必须出现在async函数中!!!!
await用在某个表达式之前,如果表达式是一个Promise,则得到的是thenable中的状态数据。
async function test1(){
console.log(1);
return 2;
}
async function test2(){
const result = await test1();
console.log(result); //2
}
test2();
等效于
function test1(){
return new Promise((resolve, reject)=>{
console.log(1);
resolve(2);
})
}
function test2(){
return new Promise((resolve, reject)=>{
test1().then(data => {
const result = data;
console.log(result); //2
resolve();
})
})
}
test2();
如果await的表达式不是Promise,则会将其使用Promise.resolve包装后按照规则运行
利用async和await 获取李华所在班级的老师的信息
查看案例:codesandbox.io/s/static-2v…
//1. 获取李华的班级id Promise
//2. 根据班级id获取李华所在班级的老师id Promise
//3. 根据老师的id查询老师信息 Promise
async function getTeacher() {
//现在处在ajax返回的promise中的then环境中
const students = await ajax({
url: "./data/students.json"
});
let cid;
for (let i = 0; i < students.length; i++) {
if (students[i].name === "李华") {
cid = students[i].classId;
}
}
//现在处在另一个ajax返回的promise中的then环境中
const classes = await ajax({
url: "./data/classes.json"
});
let tid;
for (let i = 0; i < classes.length; i++) {
if (classes[i].id === cid) {
tid = classes[i].teacherId;
}
}
//现在处在另一个ajax返回的promise中的then环境中
const teachers = await ajax({
url: "./data/teachers.json"
});
let info;
for (let i = 0; i < teachers.length; i++) {
if (teachers[i].id === tid) {
console.log(teachers[i]);
}
}
}
getTeacher();
细节:
async:
在函数前面加上async关键字,该函数就会返回一个已决阶段的promise对象,在函数的大括号里面运行的代码相当于在promise对象的未决阶段环境,是同步代码,在这里return是返回resolved状态的状态数据,默认return undefined,如果执行过程中发出错误,会返回一个rejected状态的错误信息
await:
- 如果await后面跟的是一个promise,则必须等待promise到达已决阶段的resolved状态后才执行,否则会一直等待,包括await在内的所有代码,都是在then的后续处理函数中环境中运行
- 如果await后面跟的不是Promise, (为了依旧保持异步),会通过Promise.resolve() new一个resolve状态的promise对象,然后在其then的后续处理函数中环境中运行
- 由于await只在resolve中then中运行,如果已决阶段是reject,await则会报错,导致程序无法正常执行,因此我们需要用try-catch去捕获reject
例如:
async function getPromise() {
if (Math.random() < 0.5) {
return 1
} else {
throw 2
}
}
async function test() {
try {
const result = await getPromise();
console.log('正常状态:',result);
} catch (err) {
console.log('错误状态:',err);
}
}
test()