一文搞懂事件循环机制

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

事件循环机制

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

浏览器端的事件循环

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

js 复制代码
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"); //同步任务

输出结果:

js 复制代码
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 端的事件循环机制:

js 复制代码
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);

输出结果:

js 复制代码
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 阶段。

再来一个例子:

js 复制代码
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"));
  });
});

输出结果:

js 复制代码
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 端的事件循环更复杂一些,大循环和子循环都有不同的阶段。

相关推荐
hao_04132 分钟前
elpis-core: 基于 Koa 实现 web 服务引擎架构设计解析
前端
码农黛兮_4638 分钟前
HTML、CSS 和 JavaScript 基础知识点
javascript·css·html
狂野小青年41 分钟前
npm 报错 gyp verb `which` failed Error: not found: python2 解决方案
前端·npm·node.js
鲁鲁5171 小时前
Windows 环境下安装 Node 和 npm
前端·npm·node.js
跑调却靠谱1 小时前
elementUI调整滚动条高度后与固定列冲突问题解决
前端·vue.js·elementui
软件测试媛1 小时前
软件测试——面试八股文(入门篇)
软件测试·面试·职场和发展
呵呵哒( ̄▽ ̄)"1 小时前
React - 编写选择礼物组件
前端·javascript·react.js
牛马baby1 小时前
Java高频面试之并发编程-17
java·开发语言·面试
Coding的叶子1 小时前
React Flow 简介:构建交互式流程图的最佳工具
前端·react.js·流程图·fgai·react agent
apcipot_rain6 小时前
【应用密码学】实验五 公钥密码2——ECC
前端·数据库·python