MST
星途 面试题库

面试题:JavaScript 异步编程的事件循环与微任务队列

详细描述 JavaScript 事件循环(Event Loop)的工作机制,包括宏任务队列和微任务队列的区别与联系。在一段代码中,既有 setTimeout 这样的宏任务,又有 Promise.then 这样的微任务,同时还有 async/await 函数调用,分析执行顺序,并说明原因。请编写代码验证你的分析。
43.6万 热度难度
编程语言JavaScript

知识考点

AI 面试

面试题答案

一键面试

JavaScript 事件循环(Event Loop)工作机制

  1. 调用栈(Call Stack):JavaScript 是单线程语言,调用栈用于存储函数调用。当一个函数被调用时,会将其添加到调用栈栈顶,函数执行完毕后从栈顶移除。
  2. 任务队列(Task Queue):包括宏任务队列(Macro Task Queue)和微任务队列(Micro Task Queue)。
    • 宏任务队列:常见的宏任务有 setTimeoutsetIntervalsetImmediate(Node.js 环境)、I/O 操作、UI rendering(浏览器环境)等。当调用栈为空时,事件循环会从宏任务队列中取出一个宏任务放入调用栈执行。
    • 微任务队列:常见的微任务有 Promise.thenMutationObserverprocess.nextTick(Node.js 环境)等。微任务队列在当前宏任务执行结束后,下一个宏任务开始之前执行。也就是说,每次宏任务执行完毕,会先清空微任务队列,再从宏任务队列中取下一个宏任务。

宏任务队列和微任务队列的区别与联系

  1. 区别
    • 执行时机:宏任务在调用栈为空时,每次从宏任务队列取一个执行;微任务在当前宏任务执行完毕后,下一个宏任务开始之前,清空微任务队列。
    • 任务类型:宏任务一般是与宿主环境相关的异步任务,如 I/O 操作;微任务更多是 JavaScript 语言层面的异步任务,如 Promise 链式调用。
  2. 联系:它们都是为了让 JavaScript 实现异步操作,且都依赖事件循环机制来调度执行。

代码执行顺序分析

以以下代码为例:

console.log('script start');

setTimeout(() => {
    console.log('setTimeout');
}, 0);

Promise.resolve()
   .then(() => {
        console.log('Promise.then 1');
    })
   .then(() => {
        console.log('Promise.then 2');
    });

async function asyncFunc() {
    console.log('asyncFunc start');
    await Promise.resolve();
    console.log('asyncFunc after await');
}

asyncFunc();

console.log('script end');
  1. 首先,console.log('script start') 进入调用栈执行,打印 script start,执行完毕后从调用栈移除。
  2. setTimeout 是宏任务,被放入宏任务队列。
  3. Promise.resolve().then() 是微任务,Promise.resolve() 立即 resolved,其 .then() 回调被放入微任务队列。
  4. asyncFunc() 函数调用,console.log('asyncFunc start') 进入调用栈执行,打印 asyncFunc start,执行完毕后从调用栈移除。
  5. 遇到 await Promise.resolve()await 后面的 Promise 立即 resolved,asyncFunc() 函数暂停执行,asyncFunc after await 不会立即执行,此时 await 右侧表达式执行完毕后的代码会以微任务形式放入微任务队列。
  6. console.log('script end') 进入调用栈执行,打印 script end,执行完毕后从调用栈移除。此时调用栈为空。
  7. 事件循环开始处理微任务队列,先执行 Promise.then 1 回调,打印 Promise.then 1,执行完毕后从调用栈移除;接着执行 asyncFunc after await 微任务,打印 asyncFunc after await,执行完毕后从调用栈移除;然后执行 Promise.then 2 回调,打印 Promise.then 2,执行完毕后从调用栈移除。此时微任务队列清空。
  8. 事件循环从宏任务队列取出 setTimeout 宏任务放入调用栈执行,打印 setTimeout,执行完毕后从调用栈移除。

所以最终输出顺序为:

script start
asyncFunc start
script end
Promise.then 1
asyncFunc after await
Promise.then 2
setTimeout

代码验证

上述代码在浏览器或 Node.js 环境运行,输出结果将验证上述分析。在 Node.js 环境下,直接运行该 JavaScript 文件即可;在浏览器环境,可在开发者工具的控制台中粘贴运行。