likes
comments
collection
share

一文搞懂事件循环机制

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

js 引擎是单线程的,为了保证不阻塞,js 引擎会将任务分为两种,一种是同步任务,一种是异步任务。同步任务会在主线程上排队执行,异步任务会在任务队列中排队等待,一旦主线程空闲,就会从任务队列中取出任务执行。任务队列中的任务分为两种,一种是宏任务,一种是微任务。宏任务包括:script(整体代码)、setTimeout、setInterval、setImmediate(Nodejs)、I/O(Nodejs)、UI rendering(Brower),微任务包括:process.nextTick(Nodejs)、Promise、MutationObserver(Brower)。V8 引擎通过事件循环的机制来实现异步任务的调度。

事件循环机制

  1. 每一个宏任务(注意不是宏任务队列)运行结束,会检查微任务队列,如果有微任务,就会依次执行微任务,直到微任务队列为空。
  2. 宏任务运行过程中产生的宏任务不会在当前轮次中处理,而微任务运行过程中产生的微任务会在当前轮次中处理。(因为新产生的微任务会被添加到微任务队列的末尾,而微任务队列是在当前轮次中要被清空的)

浏览器端的事件循环

通过以下代码来理解浏览器端的事件循环机制:

console.log("1"); //同步任务
setTimeout(function () {
  //宏任务
  console.log("2");
  new Promise(function (resolve) {
    //同步任务
    console.log("3");
    resolve();
  }).then(function () {
    //微任务
    console.log("4");
  });
}, 0);
new Promise(function (resolve) {
  //同步任务
  console.log("5");
  resolve();
}).then(function () {
  //微任务
  console.log("6");
  Promise.resolve().then(function () {
    //微任务
    console.log("7");
  });
});
setTimeout(function () {
  //宏任务
  console.log("8");
  new Promise(function (resolve) {
    //同步任务
    console.log("9");
    resolve();
  }).then(function () {
    //微任务
    console.log("10");
  });
});
console.log("11"); //同步任务

输出结果:

1;
5;
11;
6;
7;
2;
3;
4;
8;
9;
10;

解析:

  • 1,5,11 处于同一个同步任务按序执行,执行结束进入微任务处理阶段,此时微任务队列为[6],宏任务队列为[2,8]。

  • 6 执行过程中,7 被添加到微任务队列,此时微任务队列为[6,7],宏任务队列为[2,8]。

  • 6,7 依次执行,此时微任务队列为空,宏任务队列为[2,8]。

  • 2 处理过程中,同步任务 3 被正常执行,微任务 4 被添加到微任务队列,此时微任务队列为[4],宏任务队列为[8]。

  • 2 处理完毕,微任务队列[4]被依次执行,此时微任务队列为空,宏任务队列为[8]。

  • 8 处理过程中,同步任务 9 被正常执行,微任务 10 被添加到微任务队列,此时微任务队列为[10],宏任务队列为空。

  • 8 处理完毕,微任务队列[10]被依次执行,此时微任务队列为空,宏任务队列为空。

  • 程序运行结束。

Node 端的事件循环

Node 端的事件循环机制有点不同,Node 端的事件循环机制分为 6 个阶段,每个阶段都有一个任务队列,每个阶段都会执行完自己的任务队列,然后进入下一个阶段,这个过程会不断重复,直到程序结束。其中比较重要的三个阶段是:timers、poll、check。timers 阶段会执行 setTimeout 和 setInterval 的回调,poll 阶段会执行 I/O 回调,check 阶段会执行 setImmediate 的回调。

一文搞懂事件循环机制

每个任务执行完后,会进入一个“子循环”,这个循环由两个步骤组成:

一文搞懂事件循环机制

  • nextTick 任务是由 process.nextTick() 方法添加的,它会先于微任务执行
  • MicroTasks 任务包括:Promise、queueMicrotask()

子循环会运行到两个队列都为空,在运行期间加入的任务会被立即执行,不会等到下一轮次。

通过以下代码来理解 Node 端的事件循环机制:

function enqueueTasks() {
  Promise.resolve().then(() => console.log("Promise reaction 1"));
  queueMicrotask(() => console.log("queueMicrotask 1"));
  process.nextTick(() => console.log("nextTick 1"));
  setImmediate(() => console.log("setImmediate 1")); // (A)
  setTimeout(() => console.log("setTimeout 1"), 0);

  Promise.resolve().then(() => console.log("Promise reaction 2"));
  queueMicrotask(() => console.log("queueMicrotask 2"));
  process.nextTick(() => console.log("nextTick 2"));
  setImmediate(() => console.log("setImmediate 2")); // (B)
  setTimeout(() => console.log("setTimeout 2"), 0);
}

setImmediate(enqueueTasks);

输出结果:

nextTick 1
nextTick 2
Promise reaction 1
queueMicrotask 1
Promise reaction 2
queueMicrotask 2
setTimeout 1
setTimeout 2
setImmediate 1
setImmediate 2

解析:

  • enqueueTasks 是由 setImmediate() 方法添加的,所以它会在 check 阶段执行。enqueueTasks 方法中添加了一些任务。方法执行完会进入子循环。

  • 进入子循环。nextTick 任务是由 process.nextTick() 方法添加的,它会先于微任务执行,所以 nextTick 1 和 nextTick 2 会先于 Promise reaction 1 和 Promise reaction 2 执行。

  • 紧接着微任务被执行,包括 Promise 和 queueMicrotask

  • 子循环处理完毕,进入下一个阶段,即 timers 阶段。timers 阶段会执行 setTimeout 和 setInterval 的回调,所以 setTimeout 1 和 setTimeout 2 会被执行。

  • 我们在 check 阶段(enqueueTasks 中)添加了 immediate 任务(setImmediate 1 和 setImmediate 2),他们在最后才被打印,因为他们不会在当前轮次执行,而是等到下一个 check 阶段。

再来一个例子:

setImmediate(() => {
  setImmediate(() => console.log("setImmediate 1"));
  setTimeout(() => console.log("setTimeout 1"), 0);

  process.nextTick(() => {
    console.log("nextTick 1");
    process.nextTick(() => console.log("nextTick 2"));
  });

  queueMicrotask(() => {
    console.log("queueMicrotask 1");
    queueMicrotask(() => console.log("queueMicrotask 2"));
    process.nextTick(() => console.log("nextTick 3"));
  });
});

输出结果:

nextTick 1
nextTick 2
queueMicrotask 1
queueMicrotask 2
nextTick 3
setTimeout 1
setImmediate 1

解析:

  • Nexttick 任务被最先执行

  • "nextTick 2" 是在 nextTick 阶段被添加的,所以会在 "nextTick 1" 之后立即执行

  • microtasks 同理。

  • "nextTick 3" 是在 microtasks 阶段被添加的,所以会在 microtasks 任务清空后,子循环再次检查 nextTick 队列时执行。

  • 子循环清空,进入 timers 阶段,执行 setTimeout 1。

  • 进入 check 阶段,执行 setImmediate 1。

总结

以上就是浏览器端和 Node 端的事件循环机制,两者有点不同,也有些相似。他们之间宏任务与微任务之间切换的逻辑基本相同。Node 端的事件循环更复杂一些,大循环和子循环都有不同的阶段。