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

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

相关推荐
yujunlong39194 小时前
Redux Toolkit (RTK) + TypeScript
前端·typescript·react
AI视觉网奇4 小时前
live2d 单图转模型 单图生成模型
java·前端·python
weixin_395448915 小时前
“一次性拼接 RM+FSD 做单次前向/反向”的方案
前端·javascript·推荐算法
一只爱吃糖的小羊5 小时前
深入 React 原理:Reconciliation
前端·javascript·react.js
哆啦A梦15885 小时前
商城后台管理系统 03 Vue项目-实现表格导出EXCEL表格
前端·vue.js·excel
程序员爱钓鱼5 小时前
BlackHole 2ch:macOS无杂音录屏与系统音频采集完整技术指南
前端·后端·设计模式
未来之窗软件服务5 小时前
幽冥大陆(五十二)V10酒店门锁SDK TypeScript——东方仙盟筑基期
前端·javascript·typescript·酒店门锁·仙盟创梦ide·东方仙盟·东方仙盟sdk
LYFlied5 小时前
【每日算法】LeetCode148. 排序链表
前端·数据结构·算法·leetcode·链表
m0_738120725 小时前
应急响应——知攻善防蓝队靶机Web-1溯源过程
前端·网络·python·安全·web安全·ssh