【JavaScript面试题-基础与机制(必问)】JavaScript事件循环:宏任务、微任务的执行顺序与 async/await 的工作机制

0 阅读6分钟

各位前端小伙伴们,从今天起每天复习+收藏一篇关于JavaScript面试知识点!

一、事件循环(Event Loop)基本概念

JavaScript 是单线程语言,同一时间只能执行一个任务。为了协调异步操作(如定时器、网络请求、用户交互),JavaScript 引入了事件循环机制。

  • 调用栈(Call Stack) :执行同步代码的地方。
  • 任务队列(Task Queue) :存放待处理的宏任务。
  • 微任务队列(Microtask Queue) :存放待处理的微任务。

事件循环的流程是:

  1. 从宏任务队列中取出一个宏任务执行。
  2. 执行过程中产生的微任务会被添加到微任务队列。
  3. 宏任务执行完毕后,清空整个微任务队列(依次执行所有微任务)。
  4. 可能进行 UI 渲染(浏览器环境)。
  5. 从宏任务队列中取下一个宏任务,重复以上步骤。

一个形象的比喻

JavaScript 就像只有一个窗口的银行柜台,一次只能服务一个客户(执行一个任务)。
如果这个客户在办理业务时提出了一个“稍后处理”的需求(比如异步任务),柜员就会给他一个号,让他先去旁边等着(进入任务队列)。
这些等待的客户分成两种:

  • VIP 客户(微任务) :比如预约过的、急需处理的,优先级高。
  • 普通客户(宏任务) :比如普通排队的人。

柜员处理完当前客户后,会先叫号所有 VIP 客户(清空微任务队列),把他们的业务全部办完,然后再叫下一个普通客户(执行下一个宏任务)。
就这样循环往复,这就是事件循环。

它解决的核心问题:

  • 避免主线程阻塞:JavaScript 是单线程的,如果每次遇到耗时操作(如网络请求、文件读写)都要原地等待,整个页面就会卡死,用户无法进行任何操作。
  • 实现异步并发:通过事件循环,JavaScript 可以将异步任务交给浏览器或 Node.js 的其他线程处理,自己继续执行后续代码。当异步任务完成,对应的回调会被插入任务队列,等待主线程空闲时再执行。这样既利用了底层多线程能力,又保证了代码执行的顺序可控。

简单说,事件循环让 JavaScript 既能“一心一意”地执行代码,又能“眼观六路”地响应各种事件,是异步编程的基石。

二、宏任务(MacroTask)与微任务(MicroTask)

类型常见API执行时机
宏任务setTimeoutsetIntervalsetImmediate(Node)、I/O、UI 渲染、MessageChannel每轮事件循环执行一个
微任务Promise.then/catch/finallyMutationObserverqueueMicrotaskprocess.nextTick(Node)当前宏任务结束后、下一宏任务开始前,清空全部

注意async/await 本质也是基于 Promise 的微任务。

三、执行顺序示例

看一个经典代码,感受执行顺序:

javascript

console.log('1'); // 同步

setTimeout(() => {
  console.log('2'); // 宏任务
}, 0);

Promise.resolve().then(() => {
  console.log('3'); // 微任务
});

console.log('4'); // 同步

// 输出顺序:1, 4, 3, 2

解析

  • 先执行所有同步代码:输出 14
  • 遇到 setTimeout,将其回调放入宏任务队列。
  • 遇到 Promise.then,将其回调放入微任务队列。
  • 同步代码执行完毕,当前宏任务结束,开始清空微任务队列:输出 3
  • 微任务队列清空,可能 UI 渲染,然后取出下一个宏任务(setTimeout 回调):输出 2

四、async/await 在事件循环中的行为

async/await 是 Promise 的语法糖,其行为与 Promise 完全一致。

1. async 函数

  • async 函数总是返回一个 Promise 对象。
  • 函数体内部的返回值会被 Promise.resolve() 包装。

2. await 表达式

  • await 会暂停当前 async 函数的执行,等待后面的 Promise 完成。
  • 当 await 后面的 Promise 完成(fulfilled 或 rejected)后,会将后续代码作为一个微任务加入微任务队列。
  • 注意:await 后面的代码相当于 .then() 中的回调。

示例一:基础 await

javascript

async function foo() {
  console.log('2');
  await null; // 相当于 await Promise.resolve(null)
  console.log('4');
}

console.log('1');
foo();
console.log('3');

// 输出顺序:1, 2, 3, 4

解析

  • 同步输出 1
  • 调用 foo,同步输出 2await 之前的部分是同步执行的)。
  • 遇到 await null,立即将后续代码(console.log('4'))作为微任务加入队列,并让出线程,返回。
  • 继续执行同步代码,输出 3
  • 同步代码执行完毕,当前宏任务结束,清空微任务队列,输出 4

示例二:await 后面是一个真正的 Promise

javascript

async function bar() {
  console.log('x');
  await new Promise(resolve => {
    setTimeout(() => {
      console.log('y');
      resolve();
    }, 0);
  });
  console.log('z');
}

console.log('a');
bar();
console.log('b');

// 输出顺序:a, x, b, y, z

解析

  • 同步:a,进入 bar 同步输出 x
  • await 后是一个 Promise,该 Promise 内部有一个 setTimeout(宏任务)。await 会使 bar 函数暂停,并将后续代码(console.log('z'))暂存。
  • 继续同步输出 b
  • 同步结束,宏任务队列开始处理:执行 setTimeout 回调,输出 y,并调用 resolve()
  • 此时 Promise 状态变为 resolved,触发 bar 中 await 后面的微任务,输出 z
  • 注意:z 是在 y 之后输出的,因为 setTimeout 回调是宏任务,执行完后清空微任务队列时才会执行 z

五、综合复杂示例

javascript

async function test() {
  console.log('1');
  await new Promise(resolve => {
    console.log('2');
    resolve();
  });
  console.log('3');
}

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

new Promise(resolve => {
  console.log('5');
  resolve();
}).then(() => console.log('6'));

test();
console.log('7');

// 输出顺序:1, 5, 2, 7, 6, 3, 4

逐步分析

  1. 同步执行:

    • setTimeout 回调加入宏任务队列。

    • 执行 Promise 构造器(同步):输出 5resolve() 后 then 回调加入微任务队列。

    • 调用 test()

      • 同步输出 1
      • 执行 new Promise 构造器(同步):输出 2resolve() 后 await 后续代码(console.log('3'))加入微任务队列。
      • test 返回,继续。
    • 输出 7

  2. 同步结束,当前宏任务执行完毕,开始清空微任务队列:

    • 微任务1:then 回调输出 6
    • 微任务2:await 后续输出 3
  3. 微任务清空,取出下一个宏任务:setTimeout 回调输出 4

最终顺序1, 5, 2, 7, 6, 3, 4

六、注意事项与面试考点

  • 微任务优先级高于宏任务:每次宏任务结束后都会清空所有微任务。
  • async/await 是 Promise 的语法糖,其后续代码等同于 .then(),属于微任务。
  • Promise 构造器内部的代码是同步执行的,只有 then/catch/finally 是微任务。
  • await 后面如果不是 Promise 对象,会被 Promise.resolve() 包装
  • 事件循环在浏览器和 Node.js 中的差异(Node 中还有 process.nextTicksetImmediate 等),但面试通常以浏览器环境为主。

七、总结

理解事件循环需要记住两个核心:

  1. 执行顺序:同步代码 → 微任务 → 宏任务(循环往复)。
  2. async/await 的本质await 之前的代码同步执行,await 之后的代码作为微任务等待。

掌握这些,再配合画图分析复杂的嵌套场景,就能从容应对面试中的事件循环题目。

如果这篇这篇文章对您有帮助?关注、点赞、收藏,三连支持一下。
有疑问或想法?评论区见