一文搞懂JavaScript事件循环 (Event Loop)

你是否曾写过这样的代码,并对它的输出结果感到困惑?

javascript 复制代码
console.log('脚本开始');

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

Promise.resolve().then(() => {
  console.log('Promise 回调');
});

console.log('脚本结束');

许多开发者会下意识地认为,setTimeout 的延迟是 0 毫秒,所以它会紧接着"脚本开始"之后立即执行。

然而,最终的控制台输出却是:

javascript 复制代码
脚本开始
脚本结束
Promise 回调
setTimeout 回调

为什么会这样?为什么 Promise 的回调插在了 setTimeout 前面?

这个看似简单的现象背后,隐藏着 JavaScript 异步编程的核心------事件循环(Event Loop)


为什么我们需要事件循环?

JavaScript 是一门 单线程 语言,这意味着在任何给定时刻,它只能执行一件任务。

优点是避免了多线程竞争、锁等问题;缺点是,一旦执行耗时任务,就会阻塞所有其他操作,造成页面卡顿。

为了既保持单线程的简单性,又能处理耗时任务,JavaScript 依赖宿主环境(浏览器 / Node.js)来协作,通过事件循环调度任务的执行。


事件循环的关键组成部分

1. 调用栈 (Call Stack)

后进先出(LIFO)的数据结构,存放所有正在执行的函数调用。

2. Web APIs / Node.js APIs

宿主环境提供的能力,例如:

  • setTimeout / setInterval
  • DOM 事件监听(addEventListener
  • AJAX / Fetch 网络请求
  • Node.js 的文件 I/O 等

3. 宏任务队列 (Macrotask Queue)

先进先出的队列,存放宏任务:

  • setTimeout
  • setInterval
  • DOM 事件回调(clickscroll 等)
  • message channel

4. 微任务队列 (Microtask Queue)

先进先出的队列,存放微任务:

  • Promise.then / catch
  • MutationObserver
  • Node.js 的 process.nextTick

微任务优先级高于宏任务,一次事件循环会先清空所有微任务,再取一个宏任务执行。


DOM 事件与事件循环的关系

DOM 事件监听器是事件循环最常见的来源之一。来看一个简单例子:

html 复制代码
<button id="myBtn">点我</button>
javascript 复制代码
const myBtn = document.getElementById('myBtn');

console.log('同步代码:开始监听');

myBtn.addEventListener('click', () => {
  console.log('按钮被点击了!这是一个宏任务');
});

console.log('同步代码:监听设置完毕');

执行过程:

  1. addEventListener 同步执行,注册回调给浏览器 Web API。
  2. 浏览器后台监听点击事件,不阻塞 JavaScript 主线程。
  3. 用户点击按钮 → 浏览器将回调函数作为一个宏任务加入宏任务队列。
  4. 事件循环检测到队列有此任务,在清空微任务后执行它。

所以:DOM 事件的回调属于宏任务 ,与 setTimeout 同类。


事件循环的运转流程(图解)

graph TD A[执行调用栈中的同步任务] --> B[清空所有微任务队列] B --> C[取一个宏任务进入调用栈执行] C --> A

简化流程如下:

  1. 执行 同步任务(调用栈)。
  2. 清空 微任务队列(一次性全部)。
  3. 执行 一个 宏任务。
  4. 重复以上过程。

实战分析:开头示例的执行顺序

javascript 复制代码
console.log('脚本开始');

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

Promise.resolve().then(() => {
  console.log('Promise 回调');
});

console.log('脚本结束');

执行过程:

  1. 输出:脚本开始
  2. 注册 setTimeout 回调(宏任务)
  3. 注册 Promise.then 回调(微任务)
  4. 输出:脚本结束
  5. 清空微任务队列 → 输出:Promise 回调
  6. 执行宏任务队列 → 输出:setTimeout 回调

更多任务类型分类表

来源 类型 优先级
setTimeout / setInterval 宏任务
DOM 事件回调 宏任务
Promise.then/catch 微任务
MutationObserver 微任务
Node.js process.nextTick 微任务 最高

复杂混合场景示例

javascript 复制代码
document.body.addEventListener('click', () => {
  console.log('DOM click 宏任务');
});

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

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

(async function(){
  await Promise.resolve();
  console.log('async/await 之后的微任务');
})();

点击一次页面时可能的输出顺序:

javascript 复制代码
Promise 微任务
async/await 之后的微任务
setTimeout 宏任务
DOM click 宏任务

总结与性能优化提示

  • 宏任务 之间会执行所有微任务
  • 微任务可用于一些快速、紧急的异步逻辑(如数据校验、批量操作合并)。
  • 在需要延迟执行且不影响当前流程时,可用宏任务(如轻量的 UI 更新或延迟提示)。
  • 对性能优化的启示:
    • 合并多个 DOM 更新到一次宏任务中,减少回流/重绘。
    • 合理利用微任务处理短链异步,保持操作连贯性。

现在你已经掌握了 JavaScript 事件循环的基础与常见场景,不妨改写例子,加入更多事件和异步 API,亲自验证执行顺序。

你还有哪些和事件循环相关的经验?欢迎在评论区分享!

相关推荐
崔庆才丨静觅几秒前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅22 分钟前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅1 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment1 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅1 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊1 小时前
jwt介绍
前端
爱敲代码的小鱼1 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax
Cobyte2 小时前
AI全栈实战:使用 Python+LangChain+Vue3 构建一个 LLM 聊天应用
前端·后端·aigc
NEXT062 小时前
前端算法:从 O(n²) 到 O(n),列表转树的极致优化
前端·数据结构·算法
剪刀石头布啊2 小时前
生成随机数,Math.random的使用
前端