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

0ac87cb254a678723f7a7531ab8ead62.jpeg

英文 | 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。

结果如下:

5c313875c6688371ac0faa81ae15ee66.png

2、

例子

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

分析:

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

结果:

8520856bb9560faf884b977576da491c.png

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' 。

结果:

e39f66cecfd54490cd7913616a778120.png

4、

例子

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

分析:

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

结果:

1b5761ab311eff06242cd89bab41bb4f.png

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

5、

例子:

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

分析:

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

b9ec0e4a0c54adef43c11086cff70dc0.png

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

304e19713affe5b34019635fb97132e3.png

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

f3936020d515cc8916402541ea5e8553.png

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

d833507af904c1ef966f8c2e8e91e614.png

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

8174ac37f56bc97168ef7ac6da0fbb56.png

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

ad0d96f11d3c48e652905d201e68e76f.png

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

a3bf1c491d2800d199b2f1e57e89a88c.png

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

8e66ddabdb0bd5cdbdd159b76c97873a.png

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

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

结果:

686a8ea6154843a586ae9643dc0b311d.png

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

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

824c868669f6067c58a6f62fba4826a5.png

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

104fa652a35b2f39f2224a7a69102be5.png

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

d6c672a555d508c17bafbda68e6cb43d.png

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

3766fca7e6c34b59ddf57a7d8c5cf1b8.png

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

29580e8e27e682f8db9f06a8b5e644ac.png

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

9b48fb0b01c944ff73b39f8d59e0be59.png

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

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

73c068d9016a51a3dd2b9156cf4acbc1.png

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

d755a8e92a11929ef2583a3722224957.png

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

91d3920610bf47005d15b2849bcaed70.png

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

58f47da86efd64b3f0634cf63d3ebbcf.png

结果:

0ecd8014f11419e271063d955a1ec170.png

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 个额外的宏任务。

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

13deeeb07d2e3b52abc8641c4e51ab93.png

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

7446eb176b8391a419222e57270304f2.png

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

6e7fc1562f5537cf864eeea429479bdd.png

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

然后

0af094c8d048ac52b5578ba705d52f85.png

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

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

fa03f8f5344efb7f4a8270fd729298b9.png

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

ba656ba0762c09eb31390e77b4cddfd3.png

然后

3f63518ab8357ccdea16ce22f1477166.png

然后

f65768c77c5d50440794e8a370e85d00.png

结果

204a8ea58640f049d2f49c2b0572af49.png

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 是微任务,并且微任务优先于宏任务,所以控制台输出的顺序是不一样的。

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

2e4fdcd2b5a049c3e0d31d1fef69f96f.png

然后

1af1b22d178d1316cf7d088f7d0e22a1.png

然后

9c48df46a99f61c149a189168cec8107.png

然后

68bada1e14a47ecd8461acfe1318c333.png

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

0b2323c40ff1a0ab845a1c600dac4261.png

然后

8882478e0d53c56f72cb792f09c23eff.png

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

44c9716e4d25381537224efe8d83a392.png

然后结束。

1b333184b85adb28023cb54ba90f3d21.png

结果:

245ab0dc3e1e31b6de7aafad3cc8bd60.png

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

分析:

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

31937d2c12d39a61f5e66871d54266a1.png

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

51d23e69e99f885d2d7bde4cad606012.png

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

745e05cd7a63cf2e56a9c1b662fe41ae.png

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

6c0755445ac13fb1304c69faacbf2b95.png

然后

8054b59af2231392b4f1e5742caa28ca.png

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

7c103d5d4782d448f1455b765edb7897.png

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

2adab65bdcd6d6fce131816a2fbb8753.png

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

2ef069b01a29f99c0af2f979a910afb8.png

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

31222bc43b63fd1cf423ea5e1704f8f2.png

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

755aa2eedd414537d4190284c102fec8.png

最后

ffc5f5f590ef0d016c3618b514f69f76.png

结果

468b818cd2421cc8eeb1a189fe426c79.png

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);} 被执行。

结果:

fed87831fbb06d3d0e2f0b60bdf9947c.png

总结

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

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

学习更多技能

请点击下方公众号

bf6c0bb47af40b5873f030a853e5c6b7.gif

a642bb9075cf8d7bc9260c9d978ff197.jpeg

cba426b00e8894b7d973ce0d53b9a2f1.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值