什么是事件循环?
想象你在餐厅当服务员。你同时端着三桌客人的订单: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()。
事件循环引擎 — 不断检查调用栈是否为空,为空就从微任务队列取任务,取完再去宏任务队列取一个。
具体流程是这样的:
- 执行同步代码,压入调用栈
- 调用栈清空后,执行微任务队列中的所有任务
- 从宏任务队列取出一个任务执行
- 重复步骤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异步编程的基石。记住一句话:同步代码优先,微任务次之,宏任务最后。 理解了这一点,写异步代码就不会被执行顺序搞晕了。
下次再遇到"为什么我的回调没按预期执行"的问题,先想想事件循环在干嘛。