JavaScript从诞生之初就是一门单线程语言,一个线程同一时刻只能执行一段代码。如果所有操作都同步执行,那么一次网络请求卡住三秒钟,整个页面就会冻结三秒,用户什么都做不了。为了解决这个问题,JavaScript引入了事件循环机制,把耗时的操作交给浏览器的其他线程处理,等结果就绪后再通过任务队列通知主线程执行回调。理解事件循环,是掌握JavaScript异步编程的基础。

事件循环的完整工作流程
要理解事件循环,首先要弄清楚几个核心组成部分。第一个是调用栈,也就是执行代码的地方。函数被调用时压入栈中,执行完毕后弹出。如果调用栈不为空,主线程就一直忙着,别的事情都干不了。
第二个是Web API或者Node.js的底层API。当调用栈遇到setTimeout、fetch、DOM事件绑定这类异步调用时,并不会等待它完成,而是把它交给运行环境的其他线程去处理,主线程继续执行后面的同步代码。
第三个就是任务队列。异步操作完成后,对应的回调函数会被放入任务队列中排队。事件循环的角色就是一个永不停歇的调度员,它反复执行一个循环:查看调用栈是否为空,如果为空,就从任务队列中取出第一个任务,压入调用栈执行。这个过程可以用一段伪代码描述:
while (true) {
// 1. 等待调用栈清空
if (callStack.isEmpty()) {
// 2. 清空所有微任务队列
while (microtaskQueue.hasTasks()) {
microtaskQueue.dequeue().run();
}
// 3. 取出一个宏任务执行
if (macrotaskQueue.hasTasks()) {
macrotaskQueue.dequeue().run();
}
}
}
这段伪代码揭示了事件循环最重要的一个细节:宏任务和微任务并不是放在同一个队列里平等待遇,每执行完一个宏任务,引擎都会先把微任务队列彻底清空,再去取下一个宏任务。这个优先级差异是无数面试题的考点。
宏任务与微任务的区别及执行顺序
任务队列实际上分为两类。宏任务队列存放的是setTimeout、setInterval、setImmediate(Node.js环境)、I/O操作、UI渲染等回调;微任务队列存放的是Promise.then的回调、MutationObserver回调以及queueMicrotask注册的函数。微任务的产生往往伴随着当前宏任务的执行,比如Promise在当前宏任务中状态变为已兑现,它的then回调就会进入微任务队列。
两者的关键区别在于执行时机。微任务会在当前宏任务结束后立即全部执行,而宏任务要等到下一轮事件循环才有机会执行。换句话说,无论微任务队列中积压了多少回调,它们都会抢在下一个宏任务之前跑完。看一个经典例子:
console.log('script start'); // 同步代码
setTimeout(function() {
console.log('setTimeout'); // 宏任务
}, 0);
Promise.resolve().then(function() {
console.log('promise1'); // 微任务
}).then(function() {
console.log('promise2'); // 微任务,链式调用的第二个
});
console.log('script end'); // 同步代码
// 输出顺序:
// script start
// script end
// promise1
// promise2
// setTimeout
输出顺序的判断思路可以总结为三步:先执行所有同步代码,这本身是第一个宏任务;然后清空微任务队列,所以promise1和promise2先输出;最后才轮到setTimeout这个宏任务。特别提醒一点,setTimeout(fn, 0)并不是立即执行,它的含义是最少等待0毫秒后再把回调放入宏任务队列,实际还要受浏览器最小时间间隔(通常约4毫秒)的影响。
另外要注意,微任务中如果又产生了新的微任务,比如在then回调里再创建一个Promise,新微任务也会追加到当前微任务队列末尾,在本轮清空过程中一并执行。这就是为什么一段失控的微任务代码会让页面完全卡死,因为事件循环根本走不到下一个宏任务,渲染也无法进行。
实战中的典型场景与常见误区
掌握事件循环之后,很多日常开发中的现象就能解释了。比如在事件处理函数中修改DOM后立即读取布局信息,浏览器并不会在中间插入渲染,因为渲染发生在宏任务之间。而Promise.resolve().then经常被用来把一段代码推迟到当前同步逻辑执行完之后,同时又不耽误本该在同一次更新中完成的状态计算。
一个常见误区是认为async/await和事件循环无关。实际上,await后面的代码相当于被放进了then回调中,属于微任务。看下面的对比:
async function foo() {
console.log('foo start');
await bar(); // await 之后的代码会进入微任务
console.log('foo end'); // 等价于 then 回调,微任务
}
function bar() {
console.log('bar');
}
console.log('outer');
foo();
setTimeout(function() {
console.log('timeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise');
});
// 输出顺序:
// outer
// foo start
// bar
// promise
// foo end
// timeout
foo end排在promise之后,是因为await暂停了foo的执行,把后续代码注册为微任务时,Promise.resolve().then的回调已经先注册了,微任务遵循先进先出的顺序。理解了这一点,面对再复杂的输出顺序题,只需要按时间线把每个回调的注册顺序和所属队列类型列出来,答案自然就清晰了。
总结一下,事件循环的本质是单线程环境下对异步回调的调度方案:调用栈执行同步代码,Web API处理耗时操作,任务队列暂存待执行的回调,事件循环按宏任务加微任务的规则不断轮转。把这四个角色和它们的协作关系吃透,JavaScript的异步行为就不再是黑盒了。
JavaScript事件循环任务队列异步编程修改时间:2026-09-15 06:34:26