前言
在上一篇文章中,我们从操作系统的进程与线程 讲起,深入浏览器的多进程架构 ,最终理解了为什么 JavaScript 被设计为一门单线程语言。
但问题也随之而来:既然是单线程,JavaScript 是如何做到"异步非阻塞"的?
为什么我们能写出 setTimeout
、Promise
、fetch
等异步代码?为什么这些操作不会卡住主线程?又是谁在背后默默替我们"安排时间"?
要回答这些问题,我们必须深入理解 JavaScript 的核心执行机制------事件循环(Event Loop) 。
事件循环并不是 JavaScript 独立完成的机制,它是 JS 引擎(如 V8)与宿主环境(如浏览器)紧密配合的结果。它背后涉及到:
- 任务队列(宏任务、微任务)
- 事件触发(用户交互、定时器、网络请求)
- 回调调度(从异步到同步的桥梁)
在这一篇中,我们将从 JavaScript 主线程的执行流程出发,结合浏览器的异步线程能力,逐步揭开事件循环的神秘面纱。你将理解:
- 为什么
Promise.then
比setTimeout
执行得更快? - 为什么
setTimeout(fn, 0)
不等于"立刻执行"? - JavaScript 单线程如何通过"事件循环"实现"看起来的异步"。
现在让我们一起,从头读懂事件循环!
为什么需要事件循环?
JavaScript 是单线程语言
- JS 最初设计时是单线程,只有一个主线程处理代码逻辑。
- 这意味着: "一次只能做一件事" ,如果遇到阻塞操作(比如死循环、长时间任务),整个页面就会"卡住"。
用户需要响应式体验
- Web 页面需要实时响应用户交互(点击、滚动、输入等)。
- 同时还需要处理异步操作(网络请求、定时器、动画等)。
- 如果没有事件循环,JS 就只能顺序执行 ,异步几乎不可能实现。
什么是事件循环(Event Loop)?
Event Loop(事件循环)是 JavaScript 单线程异步编程 的核心机制,它决定了 JS 如何执行 同步任务 和 异步任务 (如 setTimeout
、Promise
、fetch
等)。
事件循环的流程
同步代码进入 调用栈,依次执行。
遇到异步代码(如
setTimeout
)则交给 Web API 处理,继续执行后续代码(不会阻塞)。Web API 完成后,回调函数进入任务队列。
当调用栈空时(同步代码执行完),Event Loop 检查任务队列:
- 如果队列里有任务(如
setTimeout
的回调),就取出第一个,推入 调用栈 执行。- 重复上述过程。
举个例子
现在你就能够明白为什么setTimeout()
为什么总在同步代码执行完之后才会执行了:
js
console.log('start');
setTimeout(()=>{console.log('Timer')},0); // 0 ms后输出
console.log('end');
最终输出为: start
,end
,Timer
.
因为计时器属于异步任务,对于事件循环来说,要先把所有的同步任务执行完毕 之后,才会检查队列,再取出异步任务执行。
既然这样你能不能猜一猜以下代码输出顺序是怎么样的?
js
console.log('Script start');
setTimeout(() => {
console.log('setTimeout');
}, 0);
Promise.resolve().then(() => {
console.log('Promise');
});
console.log('Script end');
输出结果其实为:
text
Script start
Script end
Promise
setTimeout
嘻嘻嘻,想知道为什么吗?这就关系到了一个非常重要的概念:任务队列优先级
任务队列优先级
没错,任务队列也是有特权的,有的队列生来富贵,享受着特权,有的队列就没有那样的特权咯,自然它们的所包含的任务的紧急程度是不一样的,越紧急的任务执行的越先被执行。
任务类型&队列类型
JS 里的任务分为 两种:
1. 宏任务(Macrotasks)
宏任务是 JavaScript 引擎和浏览器之间协作处理的基本任务单位 ,每个宏任务都会进入宏任务队列(MacroTask Queue) ,并在事件循环中按顺序执行。
setTimeout
、setInterval
、DOM 事件
、script(整体代码)
、requestAnimationFrame
(仅浏览器)。- 每次 Event Loop 只执行一个宏任务。
2. 微任务(Microtasks)
微任务是比宏任务优先级更高 的一种任务。它们会被加入微任务队列(MicroTask Queue) ,在当前宏任务执行结束后立即执行。
Promise.then()
、MutationObserver
、queueMicrotask()
、process.nextTick()
。- 每次调用栈空时,会先清空所有微任务,再执行宏任务。

接下来我们来上一个栗子,深入了解一下他们的执行顺序吧!

举个栗子来了解事件循环的执行顺序
只要你做出来了这个例子,那么Event Loop
就正式被你秒杀了!
js
console.log("start");
Promise.resolve().then(() => {
console.log('Promise1');
});
setTimeout(()=>{
console.log("Timer1~")
},100);
setTimeout(()=>{
console.log("Timer2~")
},0);
Promise.resolve().then(() => {
console.log('Promise2');
});
console.log('end');
OK,my Baby,Tell me,How is it gonna be~
Look in my eyes! Baby! Tell me! Why !
他会输出什么呢!
答案是:
text
start
end
Promise1
Promise2
Timer2~
Timer1~
so,为什么呢?
解析吖~
我们知道引擎是按照一个宏任务+所有微任务 来执行代码的,因为<script>
整体是一个宏任务,所以我们的引擎会先执行整个<script>
,
首先遇到第一句,为console.log("start");
,是同步代码,直接执行!
接着遇见下一句Promise.resolve().then(() => { console.log('Promise1'); });
,为异步代码,交给对应模块处理,模块处理完毕之后,加入微任务队列
接下来,到了setTimeout(()=>{ console.log("Timer1~") },100);
,为异步代码,交给其他模块处理,处理完毕后,加入宏任务队列
再接着是setTimeout(()=>{ console.log("Timer2~") },0);
,为异步代码,交给其他模块处理,处理完毕后,加入宏任务队列
随后,是Promise.resolve().then(() => { console.log('Promise2'); });
,为异步代码,交给对应模块处理,模块处理完毕之后,加入微任务队列
最终,console.log('end');
,为同步代码,直接执行!
OK,此时的宏任务,微任务队列应该是这样的了:

接下来我们知道,此时<script>
这个宏任务执行完了,接下来就该清空所有微任务队列了 ,所以接下来要执行所有在队列的Promise
。
之后调用栈又空了,又要开始执行宏任务 ,于是进入宏任务队列 ,取出任务执行,首先取出Timer2
,再检查微任务队列,看到是空的之后,再执行下一个宏任务,也就是Timer1
,至此队列全部为空,调用栈为空,任务结束。
为什么Timer2
在Timer1
之前?
我们都知道JavaScript
代码是按照顺序执行的,先执行了Timer1
的语句,再执行了Timer2
的语句,但是它们加入到队列的时间是不一样的:
js
setTimeout(()=>{
console.log("Timer1~")
},100);
setTimeout(()=>{
console.log("Timer2~")
},0);
Timer2
用了0ms加入宏任务队列,也就是说模块处理完后立刻就加入了队列,而Timer1
要经过100ms后才会加入队列,这就造成了它们进入队列的顺序不同。
我相信这个小弯弯大家都能绕过来的吧嘻嘻嘻~
面试题
相信大家对于事件循环有了一定的了解了,大家来做几道基础(超级难)的面试题吧!
Q1:setTimeout(fn, 0)
会立即执行吗?
Q2:以下代码输出什么?
js
console.log(1);
setTimeout(() => console.log(2), 0);
Promise.resolve().then(() => console.log(3));
console.log(4);
Q3:以下代码输出什么?(难⚠⚠)
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
Q4:以下代码输出什么?(难⚠⚠⚠)
js
async function test() {
console.log("start");
await Promise.resolve();
console.log("end");
}
test();
process.nextTick(() => {
console.log("nextTick");
});
Promise.resolve().then(() => {
console.log("promise");
});
啊哈哈哈解析来咯~
解析来咯~

相信大家对于Q1
和Q2
没有什么疑问,接下来我们来补充一下Q3
和Q4
的知识点吧!
async/await
与Event Loop
async/await
实际上就是把Promise.resolve().then()
包装了一下,之所以await
语句后面的代码能够在await
代码执行后再执行,本质上是因为除await
语句以外的后面的代码全部被加入了微队列。 比如:
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
async1();
在这个例子中,我将用Promise
代替async/await
来写一下:
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
// 等价于
Promise.resolve(async2()).then(()=>{
console.log("async1 end"); // 这里的内容被加入到微队列中
})
注意:await虽然等同于Promise.then() ,但是await后面的语句在微队列中执行优先度比Promise.then()要高 让我们再看一眼Q3:
js
async function async1() {
console.log("async1 start");
await async2();
console.log("async1 end");
}
async function async2() {
console.log("async2");
}
console.log("script start");
setTimeout(() => {
console.log("setTimeout");
}, 0);
async1();
new Promise(function (resolve) {
console.log("promise1");
resolve();
}).then(function () {
console.log("promise2");
});
console.log("script end");
所以上面Q3的答案为:
text
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
process.nextTick()
process.nextTick()
是 Node.js 独有的异步 API ,用于将回调函数放入当前执行栈的末尾、在下一次事件循环开始前立即执行 。它的优先级比 Promise.then()
和 setImmediate()
更高,是 Node.js 中最"急切"的微任务。
也就是说在Node.js执行模式下,它的优先级是微队列中最高的。 我们再看一眼Q4:
js
async function test() {
console.log("start");
await Promise.resolve();
console.log("end");
}
test();
process.nextTick(() => {
console.log("nextTick");
});
Promise.resolve().then(() => {
console.log("promise");
});
所以Q4答案为:
text
start
nextTick
end
promise
总结
通过本篇文章的学习我们知道了 事件循环(Event Loop) 的运行机制,即一次宏任务
->所有微任务
->一次宏任务
->所有微任务
的循环,在此我们又学习了两种任务队列与任务类型,微任务队列优先级 >宏任务队列优先级 ,我们由此也了解了JavaScript
作为单线程语言的局限性 和事件循环给它带来的各种无敌的能力,使它能够处理更复杂的逻辑情况,实现更多的功能。
最后我们又了解了async/await
的微任务优先级大于 Promise.then()
的优先级,并且在Node.js的模式下,process.nextTick()
在微队列优先级最高。
这一期就是这样了,如果客官您还看着舒服,就给个赞吧!如果有错误,也欢迎各位指出~
如果你看的不爽的话.......

略略略略略~