深度解析JavaScript中事件循环(Event Loop)的执行顺序

 更新时间:2026年05月21日 09:27:43   作者:豹哥学前端  
为什么 setTimeout 明明是 0 毫秒,却要等到最后才执行,这篇文章小编就来和大家详细介绍一下JavaScript中事件循环(Event Loop)的执行机制吧

为什么 setTimeout 明明是 0 毫秒,却要等到最后才执行?

Promise.thensetTimeout 到底谁先执行?

面试官总爱问的“事件循环”,今天一篇帮你彻底打通。

1. 从一道经典面试题说起

console.log('1');

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

Promise.resolve().then(() => console.log('3'));

console.log('4');

问:以上代码的输出顺序是什么?

很多人会脱口而出:1 2 3 41 4 2 3

正确答案是:1 4 3 2

为什么?这正是事件循环的规则导致的。

2. 事件循环是什么?为什么要它?

JavaScript 是单线程语言,同一时间只能做一件事。但我们需要处理用户点击、网络请求、定时器等异步操作。如果这些操作都排队执行,那网络请求的 200ms 就会让整个页面卡住。

事件循环就是 JS 引擎用来调度同步代码、异步回调、I/O 操作的一套机制,它让 JS 既能保持单线程,又能高效处理异步。

生活类比

  • 你有一个“待办清单”(任务队列)。
  • 你先处理手头的事(同步代码)。
  • 手头的事干完了,就去清单里看看有没有新的待办。
  • 如果有,就取出来做,做完再去看清单……
  • 这个“反复看清单并取任务执行”的过程,就是事件循环

3. 宏任务(MacroTask)与微任务(MicroTask)

在事件循环中,任务被分为两种:宏任务微任务。它们的执行优先级不同。

3.1 宏任务(MacroTask)

每次从任务队列中取出的一个任务,称为一个宏任务。

常见宏任务

  • 整体代码块(script)
  • setTimeoutsetInterval
  • I/O 操作
  • UI 渲染
  • setImmediate(Node.js)
  • requestAnimationFrame(浏览器)

3.2 微任务(MicroTask)

在当前宏任务执行完成后、下一个宏任务开始前执行的任务。

常见微任务

  • Promise.then / catch / finally
  • MutationObserver(浏览器)
  • queueMicrotask
  • process.nextTick(Node.js,优先级高于普通微任务)

3.3 优先级总结

微任务队列 > 宏任务队列

每个宏任务执行完后,会立即清空当前所有的微任务,然后再去取下一个宏任务。

4. 事件循环的工作流程(附流程图)

用文字描述:

  1. 执行一个宏任务(最开始执行的是全局脚本代码)。
  2. 执行过程中如果遇到微任务,就把它添加到微任务队列。
  3. 当前宏任务执行完毕,检查微任务队列。
  4. 依次执行微任务队列中的所有任务(直到清空)。
  5. 执行必要的 UI 渲染(浏览器)。
  6. 从宏任务队列中取出下一个宏任务,重复步骤 1。

流程图(纯文本版):

┌─────────────────────────┐
│   开始执行宏任务(script)  │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│  同步代码执行,遇到微任务入队 │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   宏任务执行完毕          │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   清空当前所有微任务      │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   (可能执行 UI 渲染)    │
└────────────┬────────────┘
             │
             ▼
┌─────────────────────────┐
│   取出下一个宏任务        │
└─────────────────────────┘

5. 用代码验证每一步

示例 1:基本顺序

setTimeout(() => console.log('宏任务'), 0);
Promise.resolve().then(() => console.log('微任务'));
console.log('同步');
// 输出:同步 → 微任务 → 宏任务

示例 2:微任务中注册新微任务

Promise.resolve().then(() => {
  console.log('微任务1');
  Promise.resolve().then(() => console.log('微任务2'));
});
console.log('同步');
// 输出:同步 → 微任务1 → 微任务2

关键:微任务队列会一次性清空,新添加的微任务也会在当前轮次执行完。

示例 3:宏任务中注册微任务

setTimeout(() => {
  console.log('宏任务');
  Promise.resolve().then(() => console.log('宏任务中的微任务'));
}, 0);
Promise.resolve().then(() => console.log('外层微任务'));
// 输出:外层微任务 → 宏任务 → 宏任务中的微任务

6. async/await 在事件循环中的特殊表现

async/await 是 Promise 的语法糖,但它在事件循环中的行为有细微的陷阱。

async function foo() {
  console.log('2');
  await bar();        // ← 这里 await 后面的代码相当于 Promise.then
  console.log('4');
}
async function bar() {
  console.log('3');
}
console.log('1');
foo();
console.log('5');
// 输出:1 2 3 5 4

分析

  • 同步:1235
  • await bar() 后面的 console.log('4') 相当于 Promise.resolve(bar()).then(() => console.log('4')),因此它进入微任务队列。
  • 当前宏任务执行完后,清空微任务,输出 4

陷阱:很多人以为 await 会阻塞,其实它只是把后续代码包装成微任务,并不会阻塞主线程。

7. Node.js 与浏览器事件循环的区别

浏览器和 Node.js 的事件循环在实现上有所不同,下面列出主要差异(Node.js 版本 11+ 后已尽量与浏览器对齐,但仍有一些区别)。

特性浏览器Node.js(v11+)
宏任务类型setTimeoutsetInterval、I/O、UI 渲染setTimeoutsetInterval、I/O、setImmediateprocess.nextTick(特殊微任务)
微任务执行时机每个宏任务之后清空微任务基本一致,但 process.nextTick 优先级高于 Promise.then
循环阶段简单:宏任务 → 微任务 → 渲染多阶段:timers → pending callbacks → idle → poll → check → close

Node.js 的事件循环有更多阶段,但作为前端开发者,你主要知道 setTimeoutPromise 的行为与浏览器基本一致即可。

8. 经典面试题集锦 + 解析

题目 1

setTimeout(() => console.log(1), 0);
Promise.resolve().then(() => console.log(2));
Promise.resolve().then(() => {
  console.log(3);
  setTimeout(() => console.log(4), 0);
});
console.log(5);

输出5 2 3 1 4

解释

  • 同步 5
  • 微任务依次 23(执行 3 时注册了一个宏任务 4)。
  • 宏任务 14

题目 2(混合 async/await)

async function async1() {
  console.log('async1 start');
  await async2();
  console.log('async1 end');
}
async function async2() {
  console.log('async2');
}
console.log('script start');
setTimeout(() => console.log('setTimeout'), 0);
async1();
new Promise((resolve) => {
  console.log('promise1');
  resolve();
}).then(() => console.log('promise2'));
console.log('script end');

输出script startasync1 startasync2promise1script endasync1 endpromise2setTimeout

逐步拆解

  1. 同步:script start
  2. 调用 async1,输出 async1 start
  3. await async2() 执行 async2,输出 async2,然后 await 后续代码(async1 end)入微任务
  4. 继续执行同步,new Promise 立即执行输出 promise1resolve 后的 then 入微任务
  5. 同步 script end
  6. 当前宏任务结束,清空微任务队列:async1 endpromise2
  7. 下一个宏任务 setTimeout 输出

题目 3(Node.js 中的process.nextTick)

setTimeout(() => console.log('timeout'), 0);
process.nextTick(() => console.log('nextTick'));
console.log('start');

Node.js 输出:startnextTicktimeout

process.nextTick 优先级高于 Promise.then,属于特殊的微任务。

9. 总结

事件循环是 JavaScript 异步执行的核心机制。记住三句话:

  1. 先同步,后异步:同步代码优先执行,异步回调在队列中等待。
  2. 微任务 > 宏任务:每个宏任务执行完后,必须清空所有微任务。
  3. await 不阻塞:它只是把后续代码包装成微任务。

实践建议

  • 日常开发中,优先用 async/await,不用手动管理 then
  • 需要并发请求时用 Promise.all
  • 记住常见宏/微任务的分类,面试时不再慌张。

到此这篇关于深度解析JavaScript中事件循环(Event Loop)的执行顺序的文章就介绍到这了,更多相关JavaScript事件循环内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • js计算两个时间之间天数差的实例代码

    js计算两个时间之间天数差的实例代码

    这篇文章主要介绍了js计算两个时间之间天数差的实例代码,有需要的朋友可以参考一下
    2013-11-11
  • script标签属性type与language使用选择

    script标签属性type与language使用选择

    很多使用javascript的朋友都有着这样一个问题:script标签属性type与language使用应如何选择,为解决此疑惑,本文详细整理了一下,需要的朋友可以参考下
    2012-12-12
  • JavaScript时间对象Date内置构造函数操作实例

    JavaScript时间对象Date内置构造函数操作实例

    这篇文章主要为大家介绍了JavaScript时间对象Date内置构造函数操作实例,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2023-05-05
  • Ajax和Comet技术总结

    Ajax和Comet技术总结

    Ajax是一种技术,一种能够向服务器请求额外的数据而无需卸载页面的技术,能够使网页具备更优的用户体验。Ajax技术的核心是XMLHttpRequest对象(XHR)。本文从XHR开始谈起,理解Ajax技术的特点,再对跨域以及Comet等技术进行简要理解和总结。下面跟着小编一起来看下吧
    2017-02-02
  • JavaScript阻止事件冒泡和默认行为的方法举例

    JavaScript阻止事件冒泡和默认行为的方法举例

    JavaScript事件的默认行为指浏览器自动执行的操作,如链接跳转或表单提交,阻止这些行为可以使用event.preventDefault()、return false或event.returnValue属性,event.stopPropagation()用于阻止事件传播,不直接阻止默认行为,需要的朋友可以参考下
    2024-10-10
  • 浅析JavaScript如何优雅捕获异常并提供合理的回退方案

    浅析JavaScript如何优雅捕获异常并提供合理的回退方案

    在 JavaScript 开发中,异常处理是保证代码健壮性的关键环节,本文将深入探讨 JavaScript 中的异常捕获机制,涵盖 try...catch,Promise 错误处理,async/await 异常捕获,希望对大家有所帮助
    2026-04-04
  • 两行代码轻松搞定JavaScript日期验证

    两行代码轻松搞定JavaScript日期验证

    两行代码轻松搞定JavaScript日期验证,通过实例化Date对象来生成一个合法的日期,验证日期是否合法,感兴趣的小伙伴们可以参考一下
    2016-08-08
  • JavaScript队列、优先队列与循环队列

    JavaScript队列、优先队列与循环队列

    这篇文章主要为大家详细介绍了JavaScript队列、优先队列与循环队列的相关资料,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-11-11
  • js实现楼层效果的简单实例

    js实现楼层效果的简单实例

    下面小编就为大家带来一篇js实现楼层效果的简单实例。小编觉得挺不错的,现在就分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2016-07-07
  • JS(JQuery)操作Array的相关方法介绍

    JS(JQuery)操作Array的相关方法介绍

    本篇文章主要是对JS(JQuery)操作Array的相关方法进行了详细的介绍,需要的朋友可以过来参考下,希望对大家有所帮助
    2014-02-02

最新评论