一、为什么需要事件循环?
JavaScript 是单线程的,意思是它一次只能做一件事。如果所有任务都排队等着,遇到一个耗时任务(比如网络请求、定时器、读取大文件),整个页面就会卡住,什么都点不了。这显然不行。
所以 JavaScript 设计了异步机制 :先执行当前任务,把耗时的任务交给浏览器(或 Node.js)去处理,等任务完成后再回头执行对应的回调。这个"回头执行"的协调机制,就是事件循环(Event Loop)。
二、事件循环怎么工作?
可以把事件循环想象成一个永不停止的工厂流水线 ,它不断从任务队列 里取出任务放到主线程执行。但任务不是随便拿的,它们分成两种:宏任务 和微任务。
1. 宏任务(MacroTask)
- 比较"大"的任务,通常由宿主环境(浏览器、Node)发起。
- 常见例子:
script整体代码(第一个宏任务)setTimeout/setInterval的回调- DOM 事件回调(如点击、键盘)
- I/O 操作(文件读写、网络请求)
setImmediate(Node 独有)
2. 微任务(MicroTask)
- 比较"小"的任务,通常由 JavaScript 引擎自身产生,优先级更高。
- 常见例子:
Promise.then/catch/finally的回调MutationObserver(浏览器监听 DOM 变化)queueMicrotask(手动添加微任务)process.nextTick(Node 独有,优先级最高)
三、执行顺序:先微后宏,一次一个宏
事件循环的每一轮(tick)是这样的:
- 执行一个宏任务 (最开始是
script整体代码)。 - 执行过程中,如果遇到微任务 (比如
Promise.then),就把它们放进"微任务队列"。 - 当前宏任务执行完毕 ,立刻清空微任务队列:依次执行所有微任务,如果在执行微任务时又产生了新的微任务,继续执行,直到微任务队列为空。
- 可能执行 UI 渲染(浏览器环境,视情况而定)。
- 从宏任务队列中取出下一个宏任务,重复以上步骤。
关键点 :微任务会在本轮宏任务结束后、下一个宏任务开始前全部执行完。所以微任务的优先级比宏任务高。
四、一个例子让你秒懂
javascript
console.log('1'); // 同步代码,属于第一个宏任务
setTimeout(() => {
console.log('2'); // 宏任务
}, 0);
Promise.resolve().then(() => {
console.log('3'); // 微任务
});
console.log('4'); // 同步代码
// 输出顺序:1, 4, 3, 2
解析:
- 第一个宏任务(
script整体代码)开始:- 输出
1 - 遇到
setTimeout,回调被放进宏任务队列 - 遇到
Promise.then,回调被放进微任务队列 - 输出
4
- 输出
- 第一个宏任务结束,检查微任务队列,执行所有微任务:
- 输出
3(微任务)
- 输出
- 微任务队列清空,取出下一个宏任务(
setTimeout回调):- 输出
2
- 输出
五、为什么要有宏任务和微任务?
- 宏任务 让异步任务能排队等待,不至于阻塞主线程。
- 微任务 则提供了一种更"紧急"的异步方式,比如 Promise 回调需要尽快执行,避免不必要的延迟。这也保证了 Promise 的回调能在当前任务结束后、下一个任务开始前立即执行。
六、一个更复杂的例子
javascript
console.log('start');
setTimeout(() => {
console.log('timeout1');
Promise.resolve().then(() => {
console.log('promise3');
});
}, 0);
Promise.resolve().then(() => {
console.log('promise1');
setTimeout(() => {
console.log('timeout2');
}, 0);
});
Promise.resolve().then(() => {
console.log('promise2');
});
console.log('end');
// 输出顺序:start, end, promise1, promise2, timeout1, promise3, timeout2
步骤拆解:
- 执行第一个宏任务(
script):输出start,把setTimeout回调加入宏任务队列,把两个Promise.then加入微任务队列,输出end。 - 清空微任务队列:依次输出
promise1、promise2。在执行promise1的回调时,又遇到setTimeout,将其回调加入宏任务队列(此时宏任务队列已有timeout1)。 - 微任务队列清空,取下一个宏任务:执行
timeout1回调,输出timeout1,同时把promise3加入微任务队列。 - 当前宏任务结束,立即清空微任务队列:输出
promise3。 - 再取下一个宏任务:执行
timeout2,输出timeout2。
七、通俗比喻
可以把事件循环想象成一个银行柜台:
- 宏任务就是来办业务的客户,每次只能接待一个(执行一个宏任务)。
- 微任务 是客户在办理业务时临时想起的"小事"(比如签个字、填个表),这些小事必须在这个客户办完业务离开前立即处理完,不能等到下一个客户来。
- 所以每个客户(宏任务)办完,柜员会先处理完他所有的零碎小事(微任务),才叫下一位。
八、总结
- 事件循环 是 JavaScript 处理异步任务的核心机制。
- 任务分两类:宏任务 (大任务,由宿主发起)和微任务(小任务,由 JS 发起)。
- 执行顺序:每轮循环先执行一个宏任务,然后清空所有微任务,接着可能渲染,再取下一个宏任务。
- 微任务优先级高于宏任务,保证了 Promise 等回调的及时性。