likes
comments
collection
share

🚀有代码在偷懒?—— 不为人知的事件循环机制

作者站长头像
站长
· 阅读数 25

有这样一段代码,我们试着输出一下:

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

事件循环执行步骤:

那么现在便可以揭晓了:事件循环执行步骤实际上是按照以下五个步骤循环执行

  1. 执行同步代码 (这属于宏任务)。
  2. 同步执行完毕后,检查是否有异步需要执行。
  3. 执行所有的微任务。
  4. 微任务执行完毕后,如果有需要就会渲染页面。
  5. 执行异步宏任务,也是开启下一次事件循环。

但单说明概念可能还是有些苍白,那下面就通过几个实例演示事件循环机制。

事件循环实例:

例一:

好了,现在我们已经了解了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. 输出1。
  2. 执行Promise,输出2。
  3. .then存入异步微任务队列。
  4. setTimeout存入异步宏任务队列。
  5. 输出7。
  6. 执行微任务.then,输出3。
  7. 发现.then中存在setTimeout,存入异步宏任务队列。
  8. 执行异步宏任务setTimeout,输出5
  9. 第一轮循环结束,开启第二轮循环。
  10. 剩余两个宏任务根据入栈循序依次执行,输出4,6,也是相当于两次循环。
🚀有代码在偷懒?—— 不为人知的事件循环机制

例三 async:

首先我们来了解一下async语法糖,它是由promise二次封装而来的函数,加在函数头部,就相当于添加了return new Promise((resolve, reject) => {}),这段代码,和awaitawait后接必须为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. 输出1
  2. 执行async1函数,因为存在await先执行async2(),输出3。
  3. 因为await,async1 end放入微任务执行队列。
  4. setTimeout进入宏任务队列。
  5. 输出5。
  6. 两个.then放入微任务队列。
  7. 输出8。
  8. 按入队列顺序执行微任务,先输出2。
  9. 再输出6。
  10. 再输出7。
  11. 微任务执行完,执行宏任务,输出4。
🚀有代码在偷懒?—— 不为人知的事件循环机制

至此,大功告成!

最后:

原来并不是有代码在偷懒。而是V8中就存在这样的事件循环机制,搞懂事件循环机制,就可以说搞懂了JS的一大核心。

转载自:https://juejin.cn/post/7398748102375309346
评论
请登录