一文搞懂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,亲自验证执行顺序。

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

相关推荐
Setsuna_F_Seiei5 小时前
前端的 AI 学习之路 01 之 Agent API 调用 - 和 Agent 的基础对话
前端·人工智能·ai编程
threerocks6 小时前
AI 原生软件开发生命周期手册 - 如何借助 AI,逐阶段改造软件开发生命周期
前端·javascript·后端
徐小夕6 小时前
3分钟从想法到Agent上线:我们开源了一款AI可视化工作流“IDE”
前端·算法·github
why技术8 小时前
eli5,我觉得这个全网在吹的技能,使用体验真的很一般啊。
前端·人工智能·后端
excel8 小时前
记录升级 nuxt3 到 nuxt4 记录
前端
剑胆琴心静水深流8 小时前
全栈之路6---web集成与呈现
前端·vue.js·spring boot·分布式·spring·前端框架·npm
前端snow8 小时前
ai agent -- Memory汇总
前端
ITresearchGuest9 小时前
AI 焦虑下,前端该何去何从
前端·人工智能
Codiggerworld9 小时前
Chrome DevTools 隐藏神技:这 5 个调试技巧你可能从来没用过
前端·chrome·chrome devtools
旋生万物9 小时前
【终极实战】用Python从零“生成“一个宇宙:螺旋干涉模型的代码实现
开发语言·前端·人工智能·react.js·php·wpf