带你理清JS事件循环(Event Loop)机制

226 阅读7分钟

本篇文章就让我们来清晰地理解一下js中的事件循环机制。

进程和线程

在理解JS事件循环机制之前,我们先要理解进程和线程的概念,计算机上的所有操作都是由cpu执行的,cpu将要执行的操作分为一个个任务,这些任务就可理解为一个个进程,而这些任务又分为一些更小的子任务,这些子任务就可称为线程。所以进程为CPU 在运行指令和保存上下文所需要的时间,而线程为执行一段指令所需要的时间

比如:浏览器每开一个tab页,就是新开一个进程,而新开的这个进程又有渲染线程,js 引擎线程,http 线程等。进程和线程都大多都可以并发执行,但是渲染线程 和 js 引擎线程是互斥的,不能并发执行,因为如果两个线程同时操作DOM,可能会导致数据不一致和页面显示错误。

event-loop

我们知道js是单线程的,v8 引擎在运行 js 代码时,这个进程中只有一个线程会被开启,也就是说它只能干一件事。这个线程负责执行所有的 js 代码,并且管理事件循环(Event Loop),所以说会有异步的概念。关于异步juejin.cn/post/744268…

先看这样一段代码了解一下事件循环;

let a = 1
console.log(a);
setTimeout(function () {
  let b = 2
  console.log(b);
  a++
  setTimeout(function () {
    b++
    console.log(b);
  }, 2000)
  console.log(b);
}, 1000)
console.log(a);

让我们先来梳理一下这段代码,从上往下执行,第一个输出1,然后碰到setTimeout是一个耗时的异步代码,会先将它挂起,先执行下面的同步代码输出1,执行完同步代码后再执行异步代码,也就是setTimeout里面的代码,输出一个2,然后a++,又碰到一个异步setTimeout代码,先挂起,执行下面的同步代码console.log(b);输出2,然后执行再执行挂起的异步代码b++ console.log(b);输出3。

image.png

这就展现了js中的事件循环,先同步再异步,异步里面再先同步再异步.....反复重复。

那么哪些是异步代码呢?

js中的代码分为同步代码和异步代码,而异步代码里面又分为微任务和宏任务,如下。

  1. 同步代码(有些耗时的代码也是同步代码)
  2. 异步代码
  • 微任务:promise.then(),process.nextTick(),MutationObserver()

  • 宏任务:script,setTimeout,setInterval,setImmediate,I/O,UI-rendering

微任务和宏任务是两种不同类型的任务队列,这两种任务的划分主要是为了更好地管理和执行异步代码,以及确保某些任务能够在其他任务之前被优先执行,微任务其中一个重要用途就是用Promise解决异步。

console.log('Script start');

setTimeout(() => {
  console.log('setTimeout callback'); // 宏任务
}, 1000);

Promise.resolve().then(() => {
  console.log('Promise then callback'); // 微任务
});

console.log('Script end');

执行顺序:

  1. “Script start”(同步代码)
  2. “Script end”(同步代码)
  3. “Promise then callback”(微任务,在当前宏任务执行完毕后立即执行)
  4. “setTimeout callback”(宏任务,在下一个事件循环迭代中执行)

eventLoop执行步骤

在js中,一段代码的执行总是先同步再异步,然后先执行微任务再执行宏任务。具体为:

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

我们来看以下代码加深对事件循环的理解;

console.log(1);
new Promise(function (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);

先分析里面的宏任务和微任务;

image.png

首先代码先执行同步代码输出 1,2,然后碰到promise.then()微任务,将其放入微任务队列中(里面包含宏任务1),然后碰到setTimeout宏任务2,然后将宏任务2 (里面包含宏任务3)放入宏任务队列中,然后执行最后一行的同步代码输出 7,此时两个队列如下。 image.png

执行完了第七行同步代码后,先执行微任务队列中的微任务,输出 3后,又碰到一个宏任务1,然后将其放入宏任务队列,此时微任务队列执行完毕,再执行宏任务队列如下; image.png

队列,先进先出,先放进去的先执行,先执行宏任务 2,输出 5,然后碰到宏任务 3,将其放入宏任务队列,宏任务2执行完毕出队列;

image.png

然后执行宏任务1,输出 4,再执行宏任务3,输出 6。归总一下,输出1 2 7 3 5 4 6

image.png

综上,事件循环的执行步骤先执行同步代码,再异步代码(里面先执行微任务,再宏任务),宏任务相当于下一次循环开头的同步代码。

await

浏览器对await 的执行提前了(await 后面的代码当成同步来执行),而且会将后续(下面)代码挤入微任务队列

接下来看看以下代码来理解;

console.log('script start');
async function async1() {
  await async2()     // 立即执行
  console.log('async1 end');    // 去到了微任务队列
}
async function async2() {
  console.log('async2 end');
}
async1()
setTimeout(() => {
  console.log('setTimeout');
}, 0)
new Promise((resolve, reject) => {
  console.log('promise');
  resolve()
})
  .then(() => {
    console.log('then1');
  })
  .then(() => {
    console.log('then2');
  });
console.log('script end');

先来看上半部分。 image.png

image.png 执行完最后一行输出script end后,然后执行微任务队列(async1 endthen1then2),再执行宏任务队列(setTimeout)。所以最后输出结果:

image.png

setTimeout 定时器执行的时间准吗?

有时面试也会被问到这个问题,答案是不准,原因如下;

setTimeout被执行时,浏览器会启动一个新的线程来计时,等到时间结束才将定时器的回条取出来执行(js 主线程将其取出),如果此时 js 还在执行同步代码,那么该回调就会被一直挂起,直到同步执行完毕微任务也执行完毕才执行该回调。

好了,关于JS事件循环机制就讲解到这里了,相信你看完会对JS中的事件循环机制有了更加清晰的认识,感觉本文对你有所帮助的话可以点点赞哦(●'◡'●)。