# 事件循环

# 写这篇文章的初衷

  1. 了解什么是事件循环
  2. 事件循环机制什么?宏任务怒我、微任务是什么?
  3. async、await执行机制
  4. node事件循环机制

# js的事件循环是什么

Js是单线程,即任务是串行执行的,后一个任务需要等待前面一个任务执行完成之后,才会开始执行,这就可能出现长时间的等待。由于Ajax网络请求、setTimeoutDOM事件的用户交互等任务在执行的时候,是不消耗CPU的,所以会形成空等现象,大大的浪费了资源。所以出现了异步:通过将任务交给相应的异步模块去处理,大大的提升了主线程的效率。

描述:主线程读取JS代码,此时为同步环境,形成相应的堆和执行栈。当有异步任务的时候,主线程会将该异步任务提交给对应的异步进程进行处理,而CPU继续处理下一个任务。当异步任务处理完成会将该任务推入任务队列中;CPU在完成任务后,在空闲时间会在任务队列中读取异步任务的callback进行后续的操作。一直重复调取并完成任务,直到所有任务完成。

image.png

# 浏览器中的事件循环

JavaScript代码的执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。整个执行过程,我们称为事件循环过程。一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。任务队列又分为macro-task(宏任务)与micro-task(微任务),在最新标准中,它们被分别称为task与jobs。

宏任务(macro-task)

  • script(整体代码)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

微任务(micro-task)

  • process.nextTick
  • Promise
  • Async/Await(实际就是promise)
  • MutationObserver(html5新特性)

async/await

async隐式返回 Promise 作为结果的函数

那么可以简单理解为,await后面的函数执行完毕时,await会产生一个微任务(Promise.then是微任务)。会将后面的代码放到微任务队列中

等到主线程同步任务结束再执行微任务队列


  window.onload = function () {
    console.log('script start'); //1

    async function async1() {
      await async2();
      console.log('async1 end');
      return Promise.resolve()
        .then(() => {
          console.log('async1-promise1');
        })
        .then(() => {
          console.log('async1-promise2');
        });
    }
    async function async2() {
      console.log('async2 end');
    }
    async1();

    setTimeout(function timer() {
      console.log('setTimeout');
    }, 0);

    new Promise(function p(resolve) {
      console.log('Promise'); // 2
      resolve();
    })
      .then(function p1() {
        console.log('promise1');
      })
      .then(function p2() {
        console.log('promise2');
      });

    console.log('script end');
  };

  /**
   *  script start
   *  async2 end
   *  Promise
   *  script end
   *  async1 end
   *  promise1
   *  async1-promise1
   *  promise2
   *  async1-promise2
   *  setTimeout
   *
   */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

我画一个流程图方便理解

iShot2023-05-19_00.25.04.png

分析这段代码:

  1. 执行代码,输出script start。
  2. 执行async1(),会调用async2(), 执行async2函数, async1中await后的代码其实是一个Promise.resolve().then()状态,因此压入微任务队列中等待执行。
  3. 遇到setTimeout,产生一个宏任务 放入宏任务队列中
  4. 遇到Promise构造函数,执行同步代码输出Promise,产生then异步微任务,因此压入微任务队列等待执行(此时微任务队列中已经塞了两个微任务)
  5. 继续向下执行代码,输出同步代码script end
  6. 此时同步代码已经执行完毕,开始遍历依次执行微任务队列, async1 end先打印,发现后面继续产生Promise.resolve().then()异步微任务放入队列等待;
  7. 此时微任务队列还有两个等待执行先输出promise1,发现后面有跟着then,因此继续放置微任务队列中,接着是 async1-promise1 以此类推 promise2、async1-promise2
  8. 微任务执行完毕,开始遍历找宏任务代码,发现一个setTimout那么就执行代码
  9. 结束流程

# node事件

浏览器中有事件循环,node 中也有,事件循环是 node 处理非阻塞 I/O 操作的机制,node中事件循环的实现是依靠的libuv引擎。由于 node 11 之后,事件循环的一些原理发生了变化,这里就以新的标准去讲,最后再列上变化点让大家了解前因后果。

# 宏任务和微任务

node 中也有宏任务和微任务,与浏览器中的事件循环类似,其中,

宏任务(macro-task)

  • setTimeout
  • setInterval
  • setImmediate
  • script(整体代码)
  • I/O 操作等。

微任务(micro-task)

  • process.nextTick(与普通微任务有区别,在微任务队列执行之前执行)
  • new Promise().then(回调)等。

# 图解node事件循环

image.png

图中的每个框被称为事件循环机制的一个阶段,每个阶段都有一个 FIFO 队列来执行回调。虽然每个阶段都是特殊的,但通常情况下,当事件循环进入给定的阶段时,它将执行特定于该阶段的任何操作,然后执行该阶段队列中的回调,直到队列用尽或最大回调数已执行。当该队列已用尽或达到回调限制,事件循环将移动到下一阶段。

因此,从上面这个简化图中,我们可以分析出 node 的事件循环的阶段顺序为:

输入数据阶段(incoming data)->轮询阶段(poll)->检查阶段(check)->关闭事件回调阶段(close callback)->定时器检测阶段(timers)->I/O事件回调阶段(I/O callbacks)->闲置阶段(idle, prepare)->轮询阶段...

# 执行流程

  1. 在 Node 当中,一段脚本总是自上而下执行,同步代码立即执行;
  2. 异步代码进入异步模块以非阻塞的方式执行,对应的异步回调函数会在异步代码执行完毕之后被派发到不同的任务队列当中
  3. 同步代码执行完毕之后,会先执行 nextTick 队列 里的任务,再执行 微任务队列,然后进入 事件循环里的队列。事件循环里有3个队列,Timer队列,Poll队列,Check队列
  • Timer 队列用于处理定时器的回调,如 setTimeOut,setInterval ;
  • Poll 队列用于处理 I/O 操作的回调,如文件读写,数据库操作,网络请求;
  • Check队列,用于处理 setImmediate 。

事件循环里的3个队列按照从上往下的顺序周而复始地执行,如果 Timer 和 Check 队列有任务,循环会一直进行,如果 Timer 和 Check 队列没有任务,会在 Poll 队列处暂停,以等待 I/O 操作,因为默认情况下,最希望处理网络请求,尽快地给客户端响应。

image.png

看这段代码

setTimeout(() => {
  console.log("setTimeOut");
}, 0);

setImmediate(() => {
  console.log("setImmediate");
});
1
2
3
4
5
6
7
  • 按照上面的说法,从上往下执行脚本时,遇到 setTimeout 会放进 Timer 队列中,遇到 setImmediate 会放进 Check 队列中,而宏任务的事件循环中,Timer 队列里的回调先执行,那么是 setTimeOut 先执行吗?

打印结果并不是我们想象这样,有时setTimeOut先打印,有时setImmediate先打印为什么会出现这种情况呢?

因为在 Node 当中,setTimeOut 的时间参数最小是 1ms,即使写了0,也会是 1ms 之后才将任务放到 宏任务 Timer 队列当中,如果系统运行得足够快,可能 Node 还没来得及将 setTimeOut 放到 Timer 队列中,而已经将 setImmediate 放入 Check 队列当中,当系统运行足够快时,事件循环机制一旦启动,就会把此时已经进 Check 队列的任务先执行,而后再去执行 Timer 队列中的 setTimeOut,所以此情况下,这两者的执行顺序是受系统运行速度影响的,不具有确定性。

如果我们想要让 setImmediate 被优先调用,可以 将 setTimeOut 和 setImmediate 放入一个 I/O 操作中,因为 I/O 操作执行时是处于 Poll 队列,下一步就要到 Check 队列了,就会把 setImmediate 里面的回调函数先执行,然后事件循环到 Timer 队列,就会执行 setTimeOut 里面的回调函数。

像这样

fs.readFile('a.text', () => {
    setTimeout(() => {
      console.log("setTimeOut");
    }, 0);

    setImmediate(() => {
      console.log("setImmediate");
    });
})
1
2
3
4
5
6
7
8
9

# 参考文档

面试题:说说事件循环机制(满分答案来了 (opens new window)

Node 事件循环 (opens new window)

最后更新时间: 5/19/2023, 12:59:23 AM