学Node,你不可能不知道Node的事件环吧

458 阅读8分钟

1.概述

  • 和浏览器中一样NodeJS中也有事件环(Event Loop)

  • 但是由于执行代码的宿主环境和应用场景不同

  • 所以两者的事件环也有所不同

2.NodeJS事件环和浏览器事件环区别

2.1任务队列个数不同

  • 浏览器事件环有2个事件队列(宏任务队列和微任务队列)

  • NodeJS事件环有6个事件队列

    ┌───────────────────────┐
┌> │timers          │执行setTimeout() 和 setInterval()中到期的callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │pending callbacks│执行系统操作的回调, 如:tcp, udp通信的错误callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │idle, prepare   │只在内部使用
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │poll            │执行与I/O相关的回调
    │                  (除了close回调、定时器回调和setImmediate()之外,几乎所有回调都执行);
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
│  │check           │执行setImmediate的callback
│  └──────────┬────────────┘
│  ┌──────────┴────────────┐
└─┤close callbacks │执行close事件的callback,例如socket.on("close",func)
    └───────────────────────┘

2.2微任务队列不同

  • 浏览器事件环中有专门存储微任务的队列

  • NodeJS事件环中没有专门存储微任务的队列

2.3微任务执行时机不同(废除)

  • 浏览器事件环中每执行完一个宏任务都会去清空微任务队列

  • NodeJS事件环中只有同步代码执行完毕和其它队列之间切换的时候回去清空微任务队列(以前的版本)

现在

Node的事件环跟浏览器的事件环基本一致,先执行同步代码,执行完以后就清空微任务队列,清空完以后按照事件环队列的优先级进行执行,每执行完一个宏任务就会去清空微任务队列

2.4微任务优先级不同

  • 浏览器事件环中如果多个微任务同时满足执行条件, 采用先进先出

  • NodeJS事件环中如果多个微任务同时满足执行条件, 会按照优先级执行

前面是不是说了常见的宏任务有Promise, MutationObserver ,process.nextTick

但是MutationObserver不属与Node的宏任务,因为这个东西是监听DOM树的变化的,你Node有DOM树吗? 是不是没有,所以Node中常见的宏任务就只有Promise,process.nextTick

那这两个哪个的优先级要高喃?

写个代码来看看就清楚了

Promise.resolve().then(function () {
  console.log("Promise");
});
process.nextTick(function () {
  console.log("process.nextTick1");
});
process.nextTick(function () {
  console.log("process.nextTick2");
});
process.nextTick(function () {
  console.log("process.nextTick3");
});

看看结果如何

由图可知,是不是process.nextTick的优先级比promise的优先级高

process.nextTick代码执行完,才会执行promise的代码

3.完整流程

先来看两个例子就基本了解Node代码执行的完整流程了

第一个例子

setTimeout(function () {
  console.log("setTimeout");
});
Promise.resolve().then(function () {
  console.log("Promise");
});
console.log("同步代码 Start");
process.nextTick(function () {
  console.log("process.nextTick");
});
setImmediate(function () {
  console.log("setImmediate");
});
console.log("同步代码 End");

我还是简单来讲解一下

从上至下执行代码,首先遇到一个setTimeout,这个是不是一个异步代码,setTimeout属于timers队列,就把这个setTimeout放入timers队列

然后遇到一个Promise,这个是不是也是一个异步代码,Promise是不是微任务,我们就暂时假设一个队列来存放Node的(微任务),把promise放入假设的队列

然后遇到一个console.log,这个是不是一个同步代码,同步代码是不是立即执行,就输出同步代码Start

接着遇到一个process.nextTick,这个是不是也是一个异步代码,process.nextTick是不是微任务,把process.nextTick放入假设的队列

之后遇到一个setImmediate,这个是不是一个异步代码,setImmediate属于check队列,就把setImmediate放入check队列

最后遇到一个console.log,这个是不是一个同步代码,同步代码是不是立即执行,就输出同步代码End

上面是不是说了先执行同步代码,同步代码执行完毕以后就会立即执行满足条件的微任务

这里同步代码是不是都执行完了,然后执行微任务,微任务中有一个promise和nextTick,在浏览器中是不是按照先进先出的原则,但是Node是按照优先级的原则来执行

前面是不是说过,nextTick的优先级比promsie的优先级高,所以会先执行nextTick代码,是不是就输出了nextTick

然后执行promise代码,静跟着输出了promise

微任务执行完毕以后,是不是就会按照Node的事件环的队列顺序来执行,setTimeout是属于timers队列的, setImmediate是属于check队列的,timers队列优先级比check高,就会先执行timers的队列就打印出了setTimeout

上面是不是说过执行完一个Node事件环中的队列以后,就会去查看微任务是否有满足条件的代码,如果满足就立即执行,不满足就不执行,执行完timers队列以后,微任务是不是没有任务,然后就会跳转到check队列,然后执行check队列,最后打印出setImmediate

如何验证我的说法

是不是来看一下执行结果

是不是就没有任何问题

下面来看第二个例子

第二个例子

setTimeout(() => {
 console.log('s1');
 Promise.resolve().then(() => {
   console.log('p1');
 })
 process.nextTick(() => {
   console.log('n1');
 })
})
console.log('start');
setTimeout(() => {
 console.log('s2');
 Promise.resolve().then(() => {
   console.log('p2');
 })
 process.nextTick(() => {
   console.log('n2');
 })
})
console.log('end');

我还是简单的讲解一下

从上至下依次执行

遇到一个setTimeout,属于异步代码不立即执行,setTimeout属于timers队列,放入timers队列

遇到console.log,属于同步代码,立即执行,打印start

又遇到一个setTimeout,属于异步代码不立即执行,setTimeout属于timers队列,放入timers队列

同步代码执行完毕就回去执行微任务,现在微任务里面没有任务,就会按照事件环的优先级来执行 当前只有一个timers队列就看里面是否有满足的条件的代码,两个是不是都满足根据先进先出的原则,s1先进来就先执行,遇到console.log,属于同步代码,立即执行,打印s1,遇到一个promise,属于异步代码不立即执行,promise属于微任务,假象一个存放微任务的队列把promise存放进去,然后遇到一个nextTick,属于异步代码不立即执行,nextTick属于微任务存放进假象的任务队列,s1就执行完毕了

s1执行完以后,上面是不是说过,执行完一个队列就会去执行微任务,看微任务里面有没有满足的代码,是不是promise和nextTick都满足,根据优先级的原则,nextTick会先执行,所以静跟着打印n1,之后执行promise,静跟着打印p1

微任务执行完毕,进行执行timers队列,s1执行完了就只剩s2了,执行s2,遇到console.log,属于同步代码,立即执行,打印s2,遇到一个promise,属于异步代码不立即执行,promise属于微任务,假象一个存放微任务的队列把promise存放进去,然后遇到一个nextTick,属于异步代码不立即执行,nextTick属于微任务存放进假象的任务队列,s2就执行完毕了

s2执行完以后,上面是不是说过,执行完一个队列就会去执行微任务,看微任务里面有没有满足的代码,是不是promise和nextTick都满足,根据优先级的原则,nextTick会先执行,所以静跟着打印n2,之后执行promise,静跟着打印p2

怎么验证我的说法?

是不是看看输出结果

是不是就没有任何问题

是不是很简单

4.面试题

注意点

当执行以下代码的时候,结果是随机的

setTimeout(() => {
  console.log("setTimeout");
});
setImmediate(() => {
  console.log("setImmediate");
});

来看看打印结果

为什么会这样?

因为在NodeJS中指定的延迟时间是有一定的误差的, 所以导致了输出结果随机的问题

我们这里没设置延迟时间是不是就表示延迟0s,因为延迟时间存在一定的误差,是不是可能延迟时间是0.1或者0.2,是不是延迟了就会立即执行下面的代码,这就是为什么存在随机性,有可能会产生误差,有可能不会产生

当然这不是面试题,只是了解一下

下面的才是面试题

面试题

下面这段代码输出的结果肯定是setImmediate先输出,setTimeout后输出

const path = require("path");
const fs = require("fs");

fs.readFile(path.join(__dirname, "04.js"), () => {
  setTimeout(() => {
    console.log("setTimeout");
  });
  setImmediate(() => {
    console.log("setImmediate");
  });
});

来看看打印结果

那到底是为什么?

我来简单的分析一下

还是老样子从上之下执行代码,遇到一个readfile,这个是不是异步代码不立即执行,readfile属于poll队列

好了代码就执行完毕了,代码执行完毕就立即执行满足条件的微任务,当前没有微任务,就按照队列的优先级来执行,当前只有一个poll队列,这个readfile满足条件就可以执行,执行readfile,首先遇到一个setTimeout,这个是不是属于异步代码不立即执行,setTimeout属于timers队列,就放入timers队列,之后遇到一个setImmediate,这个是不是属于异步代码,属于check队列,就放入check队列里,这样poll队列是不是就执行完毕了,执行完一个宏任务,是不是就会去看微任务中有没有满足条件的可以进行执行,当前是不是没有微任务,之后就会切换到check队列,为什么?因为前面说过Node事件环有顺序,poll队列的后面就是check队列,是不是应该执行完check队列才会回头执行timers队列,所以永远输出setImmediate在前面,setTimeout在后面