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


    在我们开始之前,我希望你能理清几个知识点。

    事件循环按以下顺序执行:

    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知识的面试题,希望这些面试题对你有帮助,如果你觉得有用的话,请记得点赞我,关注我,并将它分享给你身边的朋友,也许能够帮助到他。

  • 相关阅读:
    ss-5.consul服务端+生产者+消费者
    java基于微信小程序的在线学习平台 uniapp小程序
    图像修复论文阅读笔记------Image Inpainting for Irregular Holes Using Partial Convolutions
    Web3 的 10 大应用
    Java基础学习笔记(七)—— 面向对象编程(3)
    一文概览NLP句法分析:从理论到PyTorch实战解读
    Vue.js 框架源码与进阶 - Vue.js 源码剖析 - 响应式原理
    SpringBoot轻松实现项目集成Knife4j接口文档
    无序列表 – ul - li
    收录一些常见的算法题型
  • 原文地址:https://blog.csdn.net/m0_61643133/article/details/128143968