🚀有代码在偷懒?—— 不为人知的事件循环机制
有这样一段代码,我们试着输出一下:
let a = 1
console.log(a); // 输出1
setTimeout(function() {
a++
}, 1000)
console.log(a); // 输出1
令人惊奇的事情发生了,明明我们设置了一个定时器在第八行的输出之前,并且内部进行了一次a++
的操作,但是,第八行 a 的输出仍然是1,这究竟是什么原因呢?难道是代码偷懒了?
并非如此,实际上这段代码的执行先是跳过这个定时器,执行第八行 a 的输出后,再返回来执行定时器。通过观察,终端在第二次输出 1 后一秒钟才终止了进程。
这看似“偷懒”的代码,这就是本篇文章要讲的事件循环机制。
事件循环机制:
首先我们需要了解的是在V8眼中代码分为:
同步代码和异步代码:
- 同步代码:不耗时执行的代码。
- 异步代码:需要耗时执行的代码。
这里的耗时指的是在 V8 眼中的耗时,比如上面的定时器。要知道没有代码的执行是不耗时的,只有相对耗时,没有绝对耗时。
我们再来了解一下另一个概念:
进程与线程:
- 进程:CPU运行指令和保存上下文所需的事件。
- 线程:执行一段指令需要的时间。
进程和线程都属于时间单位,比方说,打开浏览器的一个页面是一个进程,而这个页面又由很多个线程组成,比如渲染线程、js引擎线程、http线程。但需要注意的是:js的引擎和页面渲染不能同时工作,js加载是会阻塞页面的加载的。
由此我们就可以知道,js代码执行是单线程的。 V8 在执行 js 的过程中,只有一个线程会工作,这样节约性能,也节约上下文切换的时间。
宏任务与微任务:
我们还需要了解的是 js 中的宏任务与微任务,这关系到下面事件循环执行步骤的讲解:
-
微任务:
promise.then
,process.nextTick()
,MutationOvserver()
-
宏任务:
script
,setTimeout
,setInterval
,setImmediate,I/O
事件循环执行步骤:
那么现在便可以揭晓了:事件循环执行步骤实际上是按照以下五个步骤循环执行。
- 执行同步代码 (这属于宏任务)。
- 同步执行完毕后,检查是否有异步需要执行。
- 执行所有的微任务。
- 微任务执行完毕后,如果有需要就会渲染页面。
- 执行异步宏任务,也是开启下一次事件循环。
但单说明概念可能还是有些苍白,那下面就通过几个实例演示事件循环机制。
事件循环实例:
例一:
好了,现在我们已经了解了V8中事件循环机制的执行过程,那么再来看这段代码,那它的执行结果又是什么?
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
})
.then(() => {
console.log(4);
})
setTimeout(() => {
console.log(5);
})
console.log(6);
不难看出,代码从上往下执行,先是执行同步代码输出1和2,到达两个.then
处将这两个微任务先不执行,放入微任务队列,再执行到定时器处,将定时器放入宏任务队列,往下执行输出6,然后循环回来执行先执行微任务队列输出3、4,再输出宏任务队列5。执行过程如下图,红色数字代表执行顺序:

输出结果如下,简直完美!

例二 相互嵌套:
那么现在,难度升级,如果宏任务微任务嵌套出现时,我们又该怎么执行呢?
console.log(1);
new Promise((resolve, reject) => {
console.log(2);
resolve()
})
.then(() => {
console.log(3);
setTimeout(() => {
console.log(4);
}, 0)
})
setTimeout(() => {
console.log(5);
setTimeout(() => {
console.log(6);
}, 0)
}, 0)
console.log(7);
依旧是遵守事件循环机制,执行所有的微任务。再执行异步宏任务,也是开启下一次事件循环,又是执行所有的微任务。再执行异步宏任务,如此反反复复。
第一次事件循环:
- 输出1。
- 执行Promise,输出2。
- .then存入异步微任务队列。
- setTimeout存入异步宏任务队列。
- 输出7。
- 执行微任务.then,输出3。
- 发现.then中存在setTimeout,存入异步宏任务队列。
- 执行异步宏任务setTimeout,输出5
- 第一轮循环结束,开启第二轮循环。
- 剩余两个宏任务根据入栈循序依次执行,输出4,6,也是相当于两次循环。

例三 async:
首先我们来了解一下async
语法糖,它是由promise
二次封装而来的函数,加在函数头部,就相当于添加了return new Promise((resolve, reject) => {})
,这段代码,和await
(await
后接必须为promise
对象)搭配使用。同时这个函数本身也是返回一个promise
对象,后面仍然可以接.then
。
async function foo() {
// return new Promise((resolve, reject) => {})
await getData() // 等待getData先执行 后接的必须为promise对象
another()
}
foo()
那么现在,这段代码的执行结果又是什么呢?
console.log('1');
async function async1() {
await async2()
console.log('2');
}
async function async2() {
console.log('3');
}
async1()
setTimeout(function() {
console.log('4');
}, 0)
new Promise(function(resolve, reject) {
console.log('5');
resolve()
})
.then(() => {
console.log('6');
})
.then(() => {
console.log('7');
})
console.log('8');
其实只需要牢记一点,await
会将后续的代码阻塞进微任务队列。
- 输出1
- 执行async1函数,因为存在await先执行async2(),输出3。
- 因为await,async1 end放入微任务执行队列。
- setTimeout进入宏任务队列。
- 输出5。
- 两个.then放入微任务队列。
- 输出8。
- 按入队列顺序执行微任务,先输出2。
- 再输出6。
- 再输出7。
- 微任务执行完,执行宏任务,输出4。

至此,大功告成!
最后:
原来并不是有代码在偷懒。而是V8中就存在这样的事件循环机制,搞懂事件循环机制,就可以说搞懂了JS的一大核心。
转载自:https://juejin.cn/post/7398748102375309346