我正在参与掘金创作者训练营第4期,点击了解活动详情,一起学习吧!
前言
作者寄语
对于Promise,你是不是这样的?
之前对于 Promise,宏任务,微任务这些概念一直都是混淆不清,每次遇到类似的面试题,我都是靠着盲猜,就算猜对了,面试官问我为什么是这样的?为什么?我也不知道为什么?你是不是也是出于这样的现状,那么就好好看看这篇文章,带你彻底搞懂晦涩难懂的Promise面试题。
本文的题目没有到特别深入,不过应该覆盖了大部分的考点,另外为了不把大家绕混,答案也没有考虑在 Node 的执行结果,执行结果全为浏览器环境下。因此如果你都会做或者有什么疑问的话,可以尽情的在评论区留言,我会一一答复的。
📢一共分为上下上下两篇文章。
💡 通过阅读本篇文章你可以学到:
- ✅ Promise 的几道基础题。
- ✅ Promise 结合 setTimeout。
- ✅ Promise 中的 then、catch、finally。
前期准备
在做下面 👇 的题目之前,我希望你能清楚几个知识点💡💡💡。
event loop 它的执行顺序:
- 📌一开始整个脚本作为一个宏任务执行
- 📌 执行过程中同步代码直接执行,宏任务进入宏任务队列,微任务进入微任务队列
- 📌 当前宏任务执行完出队,检查微任务列表,有则依次执行,直到全部执行完
- 📌 执行浏览器 UI 线程的渲染工作
- 📌 检查是否有 Web Worker 任务,有则执行
- 📌 执行完本轮的宏任务,回到 2,依此循环,直到宏任务和微任务队列都为空
微任务包括:MutationObserver、Promise.then()或 reject()、Promise 为基础开发的其它技术,比如 fetch API、V8 的垃圾回收过程、Node 独有的 process.nextTick。
宏任务包括:script 、setTimeout 、setInterval 、setImmediate 、I/O 、UI rendering。
注意 🚸:在所有任务开始的时候,由于宏任务中包括了 script,所以浏览器会先执行一个宏任务,在这个过程中你看到的延迟任务(例如 setTimeout)将被放到下一轮宏任务中来执行。
👇 下面我们从易到难,循序渐进。
📕备注:下面所有的题目可以直接复制到浏览器查看执行结果,建议大家先看题目,然后再查看答案,这样就明白自己的思路是对的还是错的,然后再看我的过程分析,就掌握的更加透彻,记忆更加的深刻了。
1.基础题型
题 1-1
题目:
const promise1 = new Promise((resolve, reject) => {
console.log('promise1');
});
console.log('1', promise1);
过程分析:
- 从上至下,先遇到 new Promise,执行该构造函数中的代码 promise1
- 然后执行同步代码 1,此时 promise1 没有被 resolve 或者 reject,因此状态还是 pending
执行结果:
'promise1'
'1' Promise{<pending>}
题 1-2
题目:
const promise = new Promise((resolve, reject) => {
console.log(1);
resolve('success');
console.log(2);
});
promise.then(() => {
console.log(3);
});
console.log(4);
过程分析:
- 从上至下,先遇到 new Promise,执行其中的同步代码 1
- 再遇到 resolve('success'), 将 promise 的状态改为了 resolved 并且将值保存下来
- 继续执行同步代码 2
- 跳出 promise,往下执行,碰到 promise.then 这个微任务,将其加入微任务队列
- 执行同步代码 4
- 本轮宏任务全部执行完毕,检查微任务队列,发现 promise.then 这个微任务且状态为 resolved,执行它。
执行结果:
1 2 4 3
题 1-3
题目:
const promise1 = new Promise((resolve, reject) => {
console.log('promise1');
resolve('resolve1');
});
const promise2 = promise1.then((res) => {
console.log(res);
});
console.log('1', promise1);
console.log('2', promise2);
过程分析:
- 从上至下,先遇到 new Promise,执行该构造函数中的代码 promise1
- 碰到 resolve 函数, 将 promise1 的状态改变为 resolved, 并将结果保存下来
- 碰到 promise1.then 这个微任务,将它放入微任务队列
- promise2 是一个新的状态为 pending 的 Promise
- 执行同步代码 1, 同时打印出 promise1 的状态是 resolved
- 执行同步代码 2,同时打印出 promise2 的状态是 pending
- 宏任务执行完毕,查找微任务队列,发现 promise1.then 这个微任务且状态为 resolved,执行它
执行结果:
'promise1'
'1' Promise{<resolved>: 'resolve1'}
'2' Promise{<pending>}
'resolve1'
好嘞,学完了这几道基础题,是不是感觉自己又行了?

2.Promise 结合 setTimeout
题 2-1
题目:
console.log('start');
setTimeout(() => {
console.log('time');
});
Promise.resolve().then(() => {
console.log('resolve');
});
console.log('end');
过程分析:
- 刚开始整个脚本作为一个宏任务来执行,对于同步代码直接压入执行栈进行执行,因此先打印出 start 和 end。
- setTimout 作为一个宏任务被放入宏任务队列(下一个)
- Promise.then 作为一个微任务被放入微任务队列
- 本次宏任务执行完,检查微任务,发现 Promise.then,执行它
- 接下来进入下一个宏任务,发现 setTimeout,执行。
执行结果:
'start'
'end'
'resolve'
'time'
题 2-2
题目:
const promise = new Promise((resolve, reject) => {
console.log(1);
setTimeout(() => {
console.log('timerStart');
resolve('success');
console.log('timerEnd');
}, 0);
console.log(2);
});
promise.then((res) => {
console.log(res);
});
console.log(4);
过程分析:
- 从上至下,先遇到 new Promise,执行该构造函数中的代码 1
- 然后碰到了定时器,将这个定时器中的函数放到下一个宏任务的延迟队列中等待执行
- 执行同步代码 2
- 跳出 promise 函数,遇到 promise.then,但其状态还是为 pending,这里理解为先不执行
- 执行同步代码 4
- 一轮循环过后,进入第二次宏任务,发现延迟队列中有 setTimeout 定时器,执行它
- 首先执行 timerStart,然后遇到了 resolve,将 promise 的状态改为 resolved 且保存结果并将之前的 promise.then 推入微任务队列
- 继续执行同步代码 timerEnd
- 宏任务全部执行完毕,查找微任务队列,发现 promise.then 这个微任务,执行它。
执行结果:
1
2
4
"timerStart"
"timerEnd"
"success"
题 2-3
题目:
Promise.resolve().then(() => {
console.log('promise1');
const timer2 = setTimeout(() => {
console.log('timer2');
}, 0);
});
const timer1 = setTimeout(() => {
console.log('timer1');
Promise.resolve().then(() => {
console.log('promise2');
});
}, 0);
console.log('start');
过程分析:
这道题稍微的难一些,在 promise 中执行定时器,又在定时器中执行 promise;
并且要注意的是,这里的 Promise 是直接 resolve 的,而之前的 new Promise 不一样。
因此执行过程为:
- 刚开始整个脚本作为第一次宏任务来执行,我们将它标记为宏 1,从上至下执行
- 遇到 Promise.resolve().then 这个微任务,将 then 中的内容加入第一次的微任务队列标记为微 1
- 遇到定时器 timer1,将它加入下一次宏任务的延迟列表,标记为宏 2,等待执行(先不管里面是什么内容)
- 执行宏 1 中的同步代码 start
- 第一次宏任务(宏 1)执行完毕,检查第一次的微任务队列(微 1),发现有一个 promise.then 这个微任务需要执行
- 执行打印出微 1 中同步代码 promise1,然后发现定时器 timer2,将它加入宏 2 的后面,标记为宏 3
- 第一次微任务队列(微 1)执行完毕,执行第二次宏任务(宏 2),首先执行同步代码 timer1
- 然后遇到了 promise2 这个微任务,将它加入此次循环的微任务队列,标记为微 2
- 宏 2 中没有同步代码可执行了,查找本次循环的微任务队列(微 2),发现了 promise2,执行它
- 第二轮执行完毕,执行宏 3,打印出 timer2
执行结果:
'start'
'promise1'
'timer1'
'promise2'
'timer2'
3.Promise 中的 then、catch、finally
题 3-1
题目:
const promise = new Promise((resolve, reject) => {
resolve('success1');
reject('error');
resolve('success2');
});
promise
.then((res) => {
console.log('then: ', res);
})
.catch((err) => {
console.log('catch: ', err);
});
过程分析:
构造函数中的 resolve 或 reject 只有第一次执行有效,多次调用没有任何作用 。得出结论:Promise 的状态一经改变就不能再改变。
执行结果:
"then: success1"
题 3-2
题目:
const promise = newPromise((resolve, reject) => {
reject('error');
resolve('success2');
});
promise
.then((res) => {
console.log('then: ', res);
})
.then((res) => {
console.log('then: ', res);
})
.catch((err) => {
console.log('catch: ', err);
})
.then((res) => {
console.log('then: ', res);
});
执行结果:
"catch: " "error"
"then3: " undefined
结论
catch 不管被连接到哪里,都能捕获上层的错误。
题 3-3
题目:
Promise.resolve(1)
.then((res) => {
console.log(res);
return2;
})
.catch((err) => {
return3;
})
.then((res) => {
console.log(res);
});
过程分析:
Promise 可以链式调用,不过 promise 每次调用 .then 或者 .catch 都会返回一个新的 promise,从而实现了链式调用, 它并不像一般我们任务的链式调用一样 return this。
上面的输出结果之所以依次打印出 1 和 2,那是因为 resolve(1)之后走的是第一个 then 方法,并没有走 catch 里,所以第二个 then 中的 res 得到的实际上是第一个 then 的返回值。且 return 2 会被包装成 resolve(2)。
执行结果:
1
2
题 3-4
题目:
Promise.resolve()
.then(() => {
returnnewError('error!!!');
})
.then((res) => {
console.log('then: ', res);
})
.catch((err) => {
console.log('catch: ', err);
});
过程分析:
猜猜这里的结果输出的是什么 🤔️ ?
你可能想到的是进入.catch 然后被捕获了错误。
结果并不是这样的,它走的是.then 里面,返回任意一个非 promise 的值都会被包裹成 promise 对象,因此这里的 return new Error('error!!!')也被包裹成了 return Promise.resolve(new Error('error!!!'))。
当然如果你抛出一个错误的话,可以用下面 👇 两的任意一种:
returnPromise.reject(newError('error!!!'));
// or
thrownewError('error!!!');
执行结果:
"then: " "Error: error!!!"
题 3-5
题目:
接下来介绍一下.then 函数中的两个参数。
第一个参数是用来处理 Promise 成功的函数,第二个则是处理失败的函数。
也就是说 Promise.resolve('1')的值会进入成功的函数,Promise.reject('2')的值会进入失败的函数。
让我们来看看这个例子 🌰:
Promise.reject('err!!!')
.then(
(res) => {
console.log('success', res);
},
(err) => {
console.log('error', err);
}
)
.catch((err) => {
console.log('catch', err);
});
执行结果:
'error' 'error!!!'
它进入的是 then()中的第二个参数里面,而如果把第二个参数去掉,就进入了 catch()中:
Promise.reject('err!!!')
.then((res) => {
console.log('success', res);
})
.catch((err) => {
console.log('catch', err);
});
执行结果:
'catch' 'error!!!'
题 3-6
接着来看看.finally(),这个功能一般不太用在面试中,不过如果碰到了你也应该知道该如何处理。
其实你只要记住它三个很重要的知识点就可以了:
- .finally()方法不管 Promise 对象最后的状态如何都会执行
- .finally()方法的回调函数不接受任何的参数,也就是说你在.finally()函数中是没法知道 Promise 最终的状态是 resolved 还是 rejected 的
- 它最终返回的默认会是一个原来的 Promise 对象值,不过如果抛出的是一个异常则返回异常的 Promise 对象。
来看看这个简单的例子 🌰:
题目:
Promise.resolve('1')
.then((res) => {
console.log(res);
})
.finally(() => {
console.log('finally');
});
Promise.resolve('2')
.finally(() => {
console.log('finally2');
return '我是finally2返回的值';
})
.then((res) => {
console.log('finally2后面的then函数', res);
});
过程分析: 这两个 Promise 的.finally 都会执行,且就算 finally2 返回了新的值,它后面的 then()函数接收到的结果却还是'2',因此打印结果为:
执行结果:
'1'
'finally2'
'finally'
'finally2后面的then函数' '2'
让我们来看一个比较难的例子 🌰:
function promise1 () {
let p = new Promise((resolve) => {
console.log('promise1');
resolve('1')
})
return p;
}
function promise2 () {
retur nnew Promise((resolve, reject) => {
reject('error')
})
}
promise1()
.then(res =>console.log(res))
.catch(err =>console.log(err))
.finally(() =>console.log('finally1'))
promise2()
.then(res =>console.log(res))
.catch(err =>console.log(err))
.finally(() =>console.log('finally2'))
过程分析:
- 首先定义了两个函数 promise1 和 promise2,先不管接着往下看。
- promise1 函数先被调用了,然后执行里面 new Promise 的同步代码打印出 promise1
- 之后遇到了 resolve(1),将 p 的状态改为了 resolved 并将结果保存下来。
- 此时 promise1 内的函数内容已经执行完了,跳出该函数
- 碰到了 promise1().then(),由于 promise1 的状态已经发生了改变且为 resolved 因此将 promise1().then()这条微任务加入本轮的微任务列表(这是第一个微任务)
- 这时候要注意了,代码并不会接着往链式调用的下面走,也就是不会先将.finally 加入微任务列表,那是因为.then 本身就是一个微任务,它链式后面的内容必须得等当前这个微任务执行完才会执行,因此这里我们先不管.finally()
- 再往下走碰到了 promise2()函数,其中返回的 new Promise 中并没有同步代码需要执行,所以执行 reject('error')的时候将 promise2 函数中的 Promise 的状态变为了 rejected
- 跳出 promise2 函数,遇到了 promise2().then(),将其加入当前的微任务队列(这是第二个微任务),且链式调用后面的内容得等该任务执行完后才执行,和.then()一样。
- OK, 本轮的宏任务全部执行完了,来看看微任务列表,存在 promise1().then(),执行它,打印出 1,然后遇到了.finally()这个微任务将它加入微任务列表(这是第三个微任务)等待执行
- 再执行 promise2().catch()打印出 error,执行完后将 finally2 加入微任务加入微任务列表(这是第四个微任务)
- OK, 本轮又全部执行完了,但是微任务列表还有两个新的微任务没有执行完,因此依次执行 finally1 和 finally2。
执行结果:
'promise1'
'1'
'error'
'finally1'
'finally2'
一口气看了这么多题,让我们喝一口 🍵,然后总结一下:
📗总结结论:
- 📝 Promise 的状态一经改变就不能再改变.
- 📝 .then 和.catch 都会返回一个新的 Promise.
- 📝 catch 不管被连接到哪里,都能捕获上层的错误。 -在 Promise 中,返回任意一个非 promise 的值都会被包裹成 promise 对象,例如 return 2 会被包装为 return Promise.resolve(2)。
- 📝 Promise 的 .then 或者 .catch 可以被调用多次, 当如果 Promise 内部的状态一经改变,并且有了一个值,那么后续每次调用.then 或者.catch 的时候都会直接拿到该值。
- 📝 .then 或者 .catch 中 return 一个 error 对象并不会抛出错误,所以不会被后续的 .catch 捕获。
- 📝 .then 或 .catch 返回的值不能是 promise 本身,否则会造成死循环。
- 📝 .then 或者 .catch 的参数期望是函数,传入非函数则会发生值穿透。
- 📝 .then 方法是能接收两个参数的,第一个是处理成功的函数,第二个是处理失败的函数,再某些时候你可以认为 catch 是.then 第二个参数的简便写法。
- 📝 .finally 方法也是返回一个 Promise,他在 Promise 结束的时候,无论结果为 resolved 还是 rejected,都会执行里面的回调函数。
⏳下期文章预告:
- Promise 中的 all 和 race
- async/await 的几道题
- 大厂面试题
写在最后
朽木不雕难成才,知耻后勇凌云志。 我是朽木白,一个热爱分享的程序猿。如果觉得本文还不错,记得点赞➕关注,说不定哪天就用得上!您的鼓励是我坚持下去的最大动力❤️❤️❤️。