10 个关于 Promise 和 setTimeout 知识的面试题,通过图解一次说透彻

web前端开发

共 6987字,需浏览 14分钟

 · 2022-08-08

英文 | https://javascript.plainenglish.io/6-interview-questions-that-combine-promise-and-settimeout-34c430fc297e

翻译 | 杨小爱


在我们开始之前,我希望你能理清几个知识点。
事件循环按以下顺序执行:
  1. JS引擎中有两个任务队列:macrotask queue和microtask queue

  2. 整个脚本最初作为宏任务执行

  3. 执行时直接执行同步代码,宏任务进入宏任务队列,微任务进入微任务队列

  4. 当前宏任务完成后,检查微任务队列,依次执行所有微任务

  5. 执行浏览器 UI 线程的渲染(您可以在本文中忽略它)

  6. 如果存在任何 Web Worker 任务,则执行它(您可以在本文中忽略这一点)

  7. 检查宏任务队列,如果不为空,则返回步骤2,执行下一个宏任务。

值得注意的是第4步:当一个macrotask完成后,先依次执行其他所有microtask,然后再执行下一个macrotask。

Mircotasks 包括:MutationObserver、Promise.then() 和 Promise.catch(),其他基于 Promise 的技术如 fetch API、V8 垃圾收集过程、node 环境中的 process.nextTick()。

Marcotasks 包括:初始脚本、setTimeout、setInterval、setImmediate、I/O、UI 渲染。

好吧,如果你不完全理解这里发生了什么,让我们用例子来练习。

一共有 10 道题:前 4 道是简单的 Promise 题,帮助你理解微任务;后面 6 个问题是 Promise 和 setTimeout 的混合。

1、

让我们从一个简单的例子开始来解释微任务。

例子:

const promise1 = new Promise((resolve, reject) => {  console.log(1);  resolve('success')});promise1.then(() => {  console.log(3);});console.log(4);

分析:

首先,执行此代码的前四行,控制台会打印出1,然后promise1就会变成resolved状态。

然后,开始执行 promise1.then(() => {console.log(3);}); 片段。因为 promise1 现在处于已解决状态,所以 () => {console.log(3);} 将立即添加到微任务队列中。

但是,我们知道 () => {console.log(3);} 是一个微任务,所以它不会立即被调用。

然后,执行最后一行代码(console.log(4);),并在控制台打印 4。

至此,所有同步的代码,即当前的宏任务,都被执行了。然后 JavaScript 引擎检查微任务队列并依次执行它们。

() => {console.log(3);} 然后执行并在控制台中打印 4。

结果如下:

2、

例子

const promise1 = new Promise((resolve, reject) => {  console.log(1);});promise1.then(() => {  console.log(3);});console.log(4);

分析:

这个例子和上一个非常相似,只是在这个例子中,promise1 会一直处于挂起状态,所以 () => {console.log(3);} 不会被执行,控制台也不会输出3。

结果:

3、

例子

const promise1 = new Promise((resolve, reject) => {  console.log(1)  resolve('resolve1')})const promise2 = promise1.then(res => {  console.log(res)})console.log('promise1:', promise1);console.log('promise2:', promise2);

仔细考虑控制台打印结果的顺序和每个 Promise 的状态。

分析:

  • 首先,前四行代码和之前一样,在控制台打印1,promise1的状态是resolved。

  • 然后,执行 const promise2 = promise1.then(...),res => {console.log(res)} 被添加到微任务队列中。同时,promise1.then() 将返回一个新的待处理的 promise 对象。

  • 然后,执行console.log('promise1:', promise1); ,控制台打印出字符串'promise1'和处于已解决状态的promise1。

  • 然后,执行console.log('promise2:', promise2); ,控制台打印出字符串‘promise2’和处于挂起状态的promise2。

  • 至此,所有同步的代码,即当前的宏任务,都被执行了。然后 JavaScript 引擎检查微任务队列并依次执行它们。

  • res => {console.log(res)} 是微任务队列中唯一的任务,现在将被执行。然后控制台将打印 'reslove1' 。

结果:

4、

例子

const fn = () => (new Promise((resolve, reject) => {  console.log(1)  resolve('success')}));fn().then(res => {  console.log(res)});console.log(2)

分析:

与之前不同的是,在这个例子中,创建 Promise 对象的行为发生在 fn 函数中。fn函数虽然是一个普通的同步函数,但并没有什么特别之处,这个例子还是很简单的。

结果:

前面的例子比较简单,现在问题会逐渐变得复杂,你准备好了吗?

5、

例子:

console.log('start')setTimeout(() => {  console.log('setTimeout')})Promise.resolve().then(() => {  console.log('resolve')})console.log('end')

分析:

首先,JS引擎中有两个任务队列:宏任务队列和微任务队列。

在程序开始时,所有的初始代码都被视为一个宏任务,被推入宏任务队列。

然后,执行第一行代码 console.log('start') 并在控制台中打印'start'。

那么 ,setTimeout(...) 就是一个等待时间为 0 的定时器,会立即执行。正如我们在本文开头提到的,setTimeout 是一个宏任务,所以 setTimeout(...), () => {console.log('setTimeout')} 的回调函数,不会立即执行,它会 被压入宏任务队列,等待稍后执行。

然后,它开始执行 Promise.resolve().then(...),并且 () => {console.log('resolve')} 被推入微任务队列。

现在,执行console.log(‘end’),在控制台打印‘end’,第一个宏任务就完成了。

当一个宏任务完成后,JS引擎会先检查微任务的队列,然后,依次执行所有的微任务。

当微任务队列为空时,JS引擎检查宏任务队列并开始执行下一个宏任务。

值得强调的是,虽然 setTimeout(...) 比 Promise.resolve().then(...) 执行得更早,但 setTimeout(...) 的回调函数仍然执行得较晚,因为 setTimeout 是一个宏任务。这是新手犯错误最多的地方。

好的,这就是上面示例代码的运行方式。我希望我的草图能帮到你。

结果:

6、

例子

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);

分析:

首先,我们暂时忽略那些回调函数,简化代码:

const promise = new Promise((resolve, reject) => {  console.log(1);  setTimeout(..., 0);  console.log(2);});promise.then(...);console.log(4);

然后我们像以前一样绘制图片。起初,所有的代码都可以被认为是一个宏任务。

然后开始执行new Promise(...),然后进入executor内部,执行console.log(1)。

然后开始执行 setTimeout(..., 0) 。定时器立即结束,其回调函数被推入宏任务队列。

然后开始执行 console.log(2) 。

现在开始执行 promise.then(...)。因为promise对象还处于pending状态,所以它的回调函数还没有压入微任务队列。也就是说,微任务队列当前仍然是空的。

然后开始执行 console.log(4) 。

至此,第一个宏任务结束,微任务队列还是空的,所以JS引擎开始下一个宏任务。

然后,开始执行 console.log('timerStart') 。

现在 resolve() 函数被执行,promise 的状态将被解析,promise.then(…) 的回调函数被推入微任务队列。

然后,开始执行 console.log('timerEnd') 。

现在当前的宏任务已经结束,JS引擎再次检查微任务队列,依次执行。

结果:

7、

例子:

const timer1 = setTimeout(() => {  console.log('timer1');const timer3 = setTimeout(() => {     console.log('timer3')  }, 0)}, 0)const timer2 = setTimeout(() => {  console.log('timer2')}, 0)console.log('start')

分析:

本例中有 3 个 setTimeout 函数,所以程序累加了 3 个额外的宏任务。

首先,让我们绘制初始宏任务队列。

然后,开始执行 timer1 对应的 setTimeout(...) 。同时,创建了一个新的宏任务。

然后,开始执行 timer2 对应的 setTimeout 。同时,另一个新的宏任务被创建。

好的,现在我们有了三个宏任务,没有微任务。

然后

现在第一个宏任务和它的执行都完成了,而微任务队列仍然是空的,JS引擎将开始执行下一个宏任务。

console.log('timer1') 被执行。

然后,开始执行 timer3 对应的 setTimeout(...) 。创建了一个新的宏任务。

然后

然后

结果

8、

例子

const timer1 = setTimeout(() => {  console.log('timer1');  const promise1 = Promise.resolve().then(() => {    console.log('promise1')  })}, 0)const timer2 = setTimeout(() => {  console.log('timer2')}, 0)console.log('start')

分析:

此示例与上一个示例类似,不同之处在于我们将其中一个 setTimeout 替换为 Promise.then。因为 setTimeout 是宏任务而 Promise.then 是微任务,并且微任务优先于宏任务,所以控制台输出的顺序是不一样的。

首先,让我们绘制初始任务队列。

然后

然后

然后

注意此时 Promise.then() 正在创建一个微任务。它的回调函数在下一个宏任务之前由 JS 引擎执行。

然后

注意,此时Promise.then()正在创建一个微任务。它的回调函数在下一个宏任务之前由 JS 引擎执行。

然后结束。

结果:

9、

例子

const promise1 = Promise.resolve().then(() => {  console.log('promise1');  const timer2 = setTimeout(() => {    console.log('timer2')  }, 0)});const timer1 = setTimeout(() => {  console.log('timer1')  const promise2 = Promise.resolve().then(() => {    console.log('promise2')  })}, 0)console.log('start');

分析:

在这个例子中,宏任务和微任务交替创建,这是一个困难的话题。如果你只是在头脑中思考,那么,很容易犯错误。但是如果你开始和我一起画图,很容易找到正确的答案。

首先,让我们绘制初始宏任务队列。

然后执行第一段代码,并创建一个微任务。

然后执行第二段代码,并创建一个宏任务

然后

当前宏任务完成,微任务队列中的任务开始。

然后,开始执行setTimeout(...)与 timer2 相关的并创建一个新的宏任务

当前的微任务队列被清空,开始下一个宏任务。

然后,创建另一个微任务。

当前宏任务已完成,JS引擎再次检查微任务队列,发现队列不为空,开始对微任务队列中的任务进行优先级排序。

最后

结果

10、

例子

const promise1 = new Promise((resolve, reject) => {  const timer1 = setTimeout(() => {    resolve('success')  }, 1000)})const promise2 = promise1.then(() => {  throw new Error('error!!!')})
console.log('promise1', promise1)console.log('promise2', promise2)
const timer2 = setTimeout(() => { console.log('promise1', promise1); console.log('promise2', promise2);}, 2000)

分析:

  • 首先,它通过new Promise(…) 创建了promise1,它处于pending 状态。还创建了一个延迟为 1 秒的计时器。

  • 然后,执行 const promise2 = promise1.then(...),因为 promise1 目前处于 Pending 状态,所以 promise1.then() 的回调函数还不会加入到微任务队列中。

  • 然后,执行 console.log('promise1', promise1) 。此时,promise1 仍处于 Pending 状态。

  • 然后,执行 console.log('promise2', promise2) 。此时,promise2 仍处于 Pending 状态。

  • 然后,执行 const timer2 = setTimeout(…) 。还创建了一个延迟为 2 秒的计时器。

  • 1000 毫秒后,timer1 完成。然后执行 thenresolve('success'),promise1 被解决。

  • 调用 promise1.then(...) 的回调函数,并执行 throw new Error('error!!!')。抛出一个错误,promise2 被拒绝。

  • 又过了 1000 毫秒,timer2 完成。() => {console.log('promise1', promise1); console.log('promise2', promise2);} 被执行。

结果:

总结

以上就是我今天与你分享的10道关于 Promise 和 setTimeout知识的面试题,希望这些面试题对你有帮助,如果你觉得有用的话,请记得点赞我,关注我,并将它分享给你身边的朋友,也许能够帮助到他。

最后,感谢你的阅读,祝编程愉快!


学习更多技能

请点击下方公众号

浏览 3
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报