🏠 首页 攻略 事件循环是什么?让JS不卡顿的幕后推手

事件循环是什么?让JS不卡顿的幕后推手

理解JavaScript的事件循环机制,掌握异步编程的核心原理

什么是事件循环?

想象你在餐厅当服务员。你同时端着三桌客人的订单:A桌要炒菜,B桌要煮面,C桌要倒水。

炒菜需要5分钟,煮面要3分钟,倒水只要10秒。你不会傻站在灶台前干等——你会先把菜下锅,然后去煮面,煮面的时候顺便给C桌倒水,等两样都差不多了再回去翻面。

事件循环(Event Loop)就是这套"不傻等"的调度机制。

在JavaScript中,所有代码默认是单线程执行的。但现实世界充满了异步操作:网络请求、定时器、用户点击、文件读写。如果每次都要等这些操作完成才能继续,网页早就卡死了。

事件循环的作用就是:主线程忙的时候,把耗时任务交给浏览器或Node.js处理;等任务完成后,再把结果"插队"到执行队列里。

为什么要懂事件循环?

不懂事件循环,你会遇到这些坑:

  • setTimeout(fn, 0) 不是立刻执行,而是"尽快"执行
  • 为什么异步回调里的变量和同步代码的执行顺序对不上?
  • 大量定时器和AJAX请求会让页面变卡吗?
  • Promise.then() 和 async/await 到底有什么区别?

搞懂了事件循环,这些问题就迎刃而解了。

事件循环的工作流程

事件循环的核心由四个部分组成:

调用栈(Call Stack) — 存放当前正在执行的函数,像一摞盘子,只能从顶部取放。

任务队列(Task Queue / Macrotask Queue) — 存放定时器和I/O回调,比如 setTimeout、setInterval、文件读取。

微任务队列(Microtask Queue) — 优先级更高,包括 Promise.then()、MutationObserver、queueMicrotask()。

事件循环引擎 — 不断检查调用栈是否为空,为空就从微任务队列取任务,取完再去宏任务队列取一个。

具体流程是这样的:

  1. 执行同步代码,压入调用栈
  2. 调用栈清空后,执行微任务队列中的所有任务
  3. 从宏任务队列取出一个任务执行
  4. 重复步骤2和3

关键点:每执行完一个宏任务,都会清空整个微任务队列。 这意味着Promise的回调会比setTimeout更早执行。

举个栗子

console.log("1. 同步代码");

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

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

console.log("4. 又是同步代码");

输出顺序是:1 → 4 → 3 → 2。

为什么?因为1和4是同步代码,先执行。Promise回调是微任务,比setTimeout这个宏任务优先级高,所以先打印3再打印2。哪怕setTimeout设的是0毫秒延迟。

实际开发中的注意事项

别在微任务里塞太多逻辑。 微任务会阻塞宏任务的执行。如果Promise链太长,页面响应会变慢。

用async/await时注意它等价于Promise。 async函数返回的就是一个Promise,await后面的代码会在微任务队列里执行。

大量DOM变化用requestAnimationFrame。 它属于宏任务,不会阻塞微任务队列,适合做动画和UI更新。

总结

事件循环是JavaScript异步编程的基石。记住一句话:同步代码优先,微任务次之,宏任务最后。 理解了这一点,写异步代码就不会被执行顺序搞晕了。

下次再遇到"为什么我的回调没按预期执行"的问题,先想想事件循环在干嘛。