LOGO OA教程 ERP教程 模切知识交流 PMS教程 CRM教程 开发文档 其他文档  
 
网站管理员

深入浅出 Event Loop:前端工程师必须掌握的运行机制

freeflydom
2025年8月8日 10:56 本文热度 88

今天想和大家聊聊一个在面试中几乎必问,但在实际工作中又容易被忽略的话题——Event Loop(事件循环)

你可能在面试时被问过:“JavaScript 是单线程的,那它是怎么实现异步的?”或者“setTimeout(fn, 0) 真的是立即执行吗?”这些问题的答案,都藏在 Event Loop 里。

别担心,即使你是第一次接触这个概念,我也尽量用最通俗的方式,带你一步步搞懂它。我们不堆术语,不讲玄学,只说人话。


一、JavaScript 的“单线程”到底是什么意思?

我们常说 JavaScript 是“单线程”的,这句话到底意味着什么?

你可以把 JavaScript 的执行环境想象成一个只有一位服务员的快餐店。这位服务员(也就是主线程)一次只能处理一件事:点餐、做汉堡、收钱……他不能同时做两件事。

比如,你让服务员“做一份汉堡,同时给我一杯可乐”,他只能先做汉堡,再倒可乐,或者反过来。他不能一边煎肉饼一边倒饮料。

在代码里,这就像这样:

console.log("第一步");
console.log("第二步");
setTimeout(() => {
  console.log("第三步(异步)");
}, 1000);
console.log("第四步");

输出结果是:

第一步
第二步
第四步
第三步(异步)

你看,setTimeout 虽然写在第三步,但它并没有立刻执行,而是被“推迟”了。为什么?因为主线程要先把当前的任务做完,才能回头处理它。

这就是“单线程”的核心:同一时间,只能做一件事


二、那异步是怎么实现的?总不能一直卡着吧?

既然 JS 是单线程的,那像 setTimeoutfetchaddEventListener 这些异步操作是怎么做到不阻塞主线程的呢?

答案是:它们不是 JS 自己做的,而是浏览器(或 Node.js 环境)帮我们做的

继续用快餐店的比喻:

  • 服务员(JS 主线程)负责点单和出餐。
  • 但厨房里的烤箱、冰箱、饮料机……这些是“浏览器提供的能力”。
  • 当你点了一个汉堡,服务员不会自己去煎,而是把订单交给厨房(异步任务),然后继续服务下一位顾客。
  • 等厨房做好了,会通知服务员:“你的汉堡好了”,服务员再把汉堡端给你。

在技术上,这个“通知”机制就是通过 任务队列(Task Queue) 实现的。


三、Event Loop 的三大核心:调用栈、任务队列、事件循环

要理解 Event Loop,你需要知道三个关键角色:

1. 调用栈(Call Stack)

这是 JS 执行函数的地方。你可以把它想象成一个“待办事项清单”,从上到下依次执行。

比如这段代码:

function a() {
  b();
  console.log("a 执行完了");
}
function b() {
  console.log("b 开始执行");
}
a();

执行过程就像这样:

  • a() 被推入调用栈
  • a 里面调用 b()b() 被推入栈
  • b() 执行完,从栈中弹出
  • a() 继续执行,打印“a 执行完了”,然后弹出

调用栈是“同步任务”的执行场所。

2. 任务队列(Task Queue / Callback Queue)

当异步任务(比如 setTimeoutsetIntervalDOM 事件Ajax 请求)完成时,它们的回调函数不会立刻执行,而是被放进一个“等待区”——这就是任务队列。

任务队列是一个先进先出(FIFO) 的队列。先进来的回调,先被执行。

3. 事件循环(Event Loop)

这才是真正的“调度员”。它的工作非常简单:

不断检查调用栈是否为空。如果为空,就从任务队列里取出第一个回调,推入调用栈执行。

就这么简单!它像个永不停歇的循环,一直盯着:

  • “栈空了吗?”
  • “空了?好,看看队列里有没有任务。”
  • “有?拿一个过来执行。”

这就是“事件循环”名字的由来:它在循环地处理事件(回调)。


四、宏任务 vs 微任务:你必须知道的细节

到这里,你以为 Event Loop 就完了?不,还有一个更精细的划分:宏任务(Macrotask)和微任务(Microtask)

1. 宏任务(Macrotask)

常见的宏任务包括:

  • setTimeout
  • setInterval
  • setImmediate(Node.js)
  • I/O 操作
  • UI 渲染(浏览器)
  • script 标签中的整体代码

2. 微任务(Microtask)

微任务的优先级更高,常见的有:

  • Promise.then/catch/finally
  • MutationObserver(监听 DOM 变化)
  • queueMicrotask()
  • process.nextTick()(Node.js)

关键区别:执行时机

Event Loop 的执行顺序是这样的:

  1. 执行一个宏任务(比如整个 script 代码)
  2. 执行过程中,遇到异步操作,把回调放进对应的队列:
    • setTimeout → 宏任务队列
    • Promise.then → 微任务队列
  3. 当前宏任务执行完,立即清空微任务队列(全部执行完)
  4. 然后去宏任务队列取下一个宏任务
  5. 重复这个过程

举个例子,彻底搞懂

来看这段经典代码:

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

输出顺序是什么?

我们一步步分析:

  1. 执行全局脚本(宏任务)

    • 打印 "1"
    • 遇到 setTimeout,把回调 () => console.log("2") 放入宏任务队列
    • 遇到 Promise.then,把回调 () => console.log("3") 放入微任务队列
    • 打印 "4"
    • 当前宏任务执行完毕
  2. 清空微任务队列

    • 执行 Promise.then 的回调,打印 "3"
  3. 取下一个宏任务

    • 执行 setTimeout 的回调,打印 "2"

所以输出是:1 → 4 → 3 → 2

注意:setTimeout(fn, 0) 并不是“立即执行”,而是“等当前所有同步和微任务执行完后,再执行”。


五、更复杂的例子:嵌套 Promise 和 setTimeout

再看一个稍微复杂点的例子:

console.log("start");
setTimeout(() => {
  console.log("timeout1");
  Promise.resolve().then(() => {
    console.log("promise in timeout");
  });
}, 0);
Promise.resolve().then(() => {
  console.log("promise1");
  setTimeout(() => {
    console.log("timeout in promise");
  }, 0);
});
console.log("end");

来,我们一步步走:

  1. 执行全局宏任务:

    • 打印 "start"
    • setTimeout → 宏任务队列
    • Promise.then → 微任务队列
    • 打印 "end"
    • 宏任务结束
  2. 清空微任务队列:

    • 执行 Promise.then,打印 "promise1"
    • 在 then 里又遇到 setTimeout,把它加入宏任务队列
    • 微任务队列清空
  3. 取下一个宏任务(第一个 setTimeout):

    • 执行,打印 "timeout1"
    • 遇到 Promise.then,加入微任务队列
  4. 清空微任务队列:

    • 执行 Promise.then,打印 "promise in timeout"
  5. 取下一个宏任务(Promise.then 里的 setTimeout):

    • 执行,打印 "timeout in promise"

最终输出:

start
end
promise1
timeout1
promise in timeout
timeout in promise

是不是有点绕?多看几遍,画个流程图,就清楚了。


六、为什么要有微任务?它有什么用?

你可能会问:既然有宏任务就够了,为啥还要搞个微任务?

答案是:为了更精细的控制和性能优化

比如:

  • Promise 的链式调用.then().then().then(),我们希望这些回调能尽快执行,而不是等一轮完整的 Event Loop。
  • 避免 UI 卡顿:微任务在当前任务结束后立即执行,不会触发页面重绘,适合做数据更新、状态同步等操作。
  • DOM 观察MutationObserver 用微任务来批量处理 DOM 变化,避免频繁重排。

简单说:微任务 = 高优先级、立即执行的小任务


七、实际开发中的影响

理解 Event Loop 不只是应付面试,它对实际开发也有帮助。

1. 避免长时间同步任务阻塞 UI

// ❌ 千万别这么干!
for (let i = 0; i < 1000000; i++) {
  // 做大量计算
}
// 这期间页面完全卡死,用户无法点击、滚动

这种长时间运行的同步代码会阻塞 Event Loop,导致页面无响应。

解决方案:拆分成小任务,用 setTimeout 或 requestIdleCallback 分批执行。

2. 正确处理异步依赖

let data;
fetch("/api/data").then(res => res.json()).then(d => data = d);
console.log(data); // undefined!

因为 fetch 是异步的,console.log 是同步的,它先执行了。

正确做法:用 async/await 或确保在回调中使用数据。

3. setTimeout(fn, 0) 的用途

虽然它不是“立即执行”,但可以用来:

  • 将任务推迟到下一轮 Event Loop
  • 让 UI 有机会先更新
  • 实现简单的“异步批处理”
// 比如:收集一批操作,最后统一处理
let queue = [];
function addTask(task) {
  queue.push(task);
  setTimeout(processQueue, 0); // 推迟到下一轮
}
function processQueue() {
  if (queue.length > 0) {
    // 处理所有任务
    queue.forEach(task => task());
    queue = [];
  }
}

八、总结:Event Loop 的完整流程

最后,我们来梳理一下浏览器中 Event Loop 的完整流程:

  1. 执行一个宏任务(如整个 script
  2. 执行过程中:
    • 遇到 setTimeout → 加入宏任务队列
    • 遇到 Promise.then → 加入微任务队列
    • 遇到 DOM 事件 → 加入宏任务队列
  3. 当前宏任务执行完毕
  4. 立即执行所有微任务(清空微任务队列)
  5. 尝试渲染页面(如果需要)
  6. 取下一个宏任务,回到第1步

记住这个口诀:

一个宏任务,清空微任务,再来下一个宏任务。


写在最后

Event Loop 是 JavaScript 异步编程的基石。它看似复杂,但核心思想很简单:用一个循环不断检查任务队列,按顺序执行任务

​转自https://juejin.cn/post/7534907614394482723


该文章在 2025/8/8 10:56:38 编辑过
关键字查询
相关文章
正在查询...
点晴ERP是一款针对中小制造业的专业生产管理软件系统,系统成熟度和易用性得到了国内大量中小企业的青睐。
点晴PMS码头管理系统主要针对港口码头集装箱与散货日常运作、调度、堆场、车队、财务费用、相关报表等业务管理,结合码头的业务特点,围绕调度、堆场作业而开发的。集技术的先进性、管理的有效性于一体,是物流码头及其他港口类企业的高效ERP管理信息系统。
点晴WMS仓储管理系统提供了货物产品管理,销售管理,采购管理,仓储管理,仓库管理,保质期管理,货位管理,库位管理,生产管理,WMS管理系统,标签打印,条形码,二维码管理,批号管理软件。
点晴免费OA是一款软件和通用服务都免费,不限功能、不限时间、不限用户的免费OA协同办公管理系统。
Copyright 2010-2025 ClickSun All Rights Reserved