事件循环

Node在启动时候会初始化event loop(事件循环),
众所周知JS是单线程事件循环方式,其中又有一些浏览器和Node各自支持的异步API,现在来说分为宏任务(macro task)和微任务(micro task)。微任务会在宏任务完成后执行,等全部微任务执行完毕再执行下一个宏任务

  • 宏任务

    浏览器Node
    整体代码(script)
    UI交互事件
    I/O
    setTimeout
    setInterval
    setImmediate
  • 微任务

    浏览器Node
    process.nextTick
    MutationObserver
    Promise.then catch finally

Node

我们知道Node下定时器其实有很多选择,一般的setTimeout,但是如果精度要求比较高的时候,setTimeout不够用。因为setTimeout一般都是延后的,而且如果事件循环阻塞了,那么还会因阻塞而延迟,甚至完全不执行回调。

setTimeout的行为可以这么来看,首先setTimeout会生成定时器放入主循环(观察者),然后事件循环会定期查看主循环里的定时器是否到时间了,到时间了就取出执行回调函数。setInterval同理,只是不是一次性取出,而是可以多次取出,所以setInterval在事件循环卡住后,后面可能会短时间执行多次回调,由此而来。

Node的事件循环open in new window

   ┌───────────────────────────┐
┌─>│           timers          │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │     pending callbacks     │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
│  │       idle, prepare       │
│  └─────────────┬─────────────┘      ┌───────────────┐
│  ┌─────────────┴─────────────┐      │   incoming:   │
│  │           poll            │<─────┤  connections, │
│  └─────────────┬─────────────┘      │   data, etc.  │
│  ┌─────────────┴─────────────┐      └───────────────┘
│  │           check           │
│  └─────────────┬─────────────┘
│  ┌─────────────┴─────────────┐
└──┤      close callbacks      │
   └───────────────────────────┘

上图每个框都称为一个阶段,如timers阶段,poll阶段处理传入的连接数据等

Node事件循环顺序

  1. timers: 执行setTimeout和setInterval的回调
  2. pending callbacks: 执行延迟到下一个循环迭代的 I/O 回调,比如TCP socket收到ECONNREFUSED,类unix系统会想上报,会在这个队列里等待执行
  3. idle, prepare: 仅系统内部使用
  4. poll: 检索新的I/O事件;执行与I/O相关的回调(除了close回调、由计时器调度的回调和setImmediate()之外,几乎所有回调都在这里执行);节点也有可能在此阻塞
  5. check: setImmediate在这里执行
  6. close callbacks: 一些关闭的回调函数,如:socket.on('close', ...)

process.nextTick比较特别,是因为Node异步的设计理念而诞生,从技术上讲,process.nextTick()不是事件循环的一部分。相反,无论事件循环的当前阶段如何,nextTickQueue都将在当前操作完成后进行处理。这里,操作被定义为来自底层C/C++处理程序的转换,并处理需要执行的JavaScript。
任何时候在给定阶段调用process.nextTick()时,传递给process.nextTick()的所有回调都将在事件循环继续之前被解析。

process.nextTick适合用在什么地方?

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);
  this.emit('event');
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});





 







MyEmitter构造函数想通知event事件监听,但是此时执行this.emit('event'),事件还没绑定,这个时候调用会有问题。但是如果使用了process.nextTick,那么可以正常执行

const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(() => {
    this.emit('event');
  });
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
  console.log('an event occurred!');
});







 
 
 







宏任务顺序

直接上例子,node为v12.18.2下本地亲测

  • macroTask
    • 整体代码、promise构造函数
    • setTimeout、setInterval
    • setImmediate
  • microTask
    • process.nextTick(虽然归到这里,但是process.nextTick并不算事件循环里的)
    • promise.then回调
process.nextTick(()=>{
  console.log(`process.nextTick 1`)
})

new Promise((resolve, reject) => {
  console.log(`Promise exec`)
  resolve()
}).then(() => {
  console.log(`Promise then exec`)
})

process.nextTick(()=>{
  console.log(`process.nextTick 2`)
})

setImmediate(() => {
  console.log(`setImmediate 1`)
  process.nextTick(() => {
    console.log(`process.nextTick 3`)
  })
})

setTimeout(() => {
  console.log(`setTimeout 0`)
}, 0) // 注意在这里setTimeout最少为1,写为0则Node执行的时候会重置为1

setInterval(() => {
  console.log(`setInterval 0`)
}, 0) // 同理,也会被重置为1

setImmediate(() => {
  console.log(`setImmediate 2`)
})

console.log(`script end`)

输出结果如下,自己试注意无限循环

Promise exec
script end
process.nextTick 1
process.nextTick 2
Promise then exec
setTimeout 0
setInterval 0
setImmediate 1
process.nextTick 3
setImmediate 2
setInterval 0
setInterval 0
……
  1. 首先扫到process.nextTick、setImmediate的时候依次顺序加入microTaskmacroTask,来到new Promise这里,Promise的构造函数是同步执行的,输出Promise exec返回,then加入microTask
  2. 继续扫setTimeout同理加入macroTask,到最后console先输出。之后按照FIFO的形式清空microTask,输出process.nextTick 1,process.nextTick 2注意这里microTask里process.nextTick优先级高,所以会先输出promise.nextTick的,之后再输出Promise.then的,Promise.then执行完毕后microTask已全部清空
  3. 开始执行下一个macroTask,同样事件循环里macroTask是有优先级的,按照上面所述,首先是timers队列的先执行,所以是setTimeout 0setInterval 0先输出,接着到poll I/O里的,没东西。然后是check阶段,执行setImmediate,输出setImmediate 1。此时又遇到process.nextTick,所以再次加入microTask
  4. 这时macroTaskmicroTask都有任务,先清空microTask再去执行下一个macroTask的任务,所以先输出process.nextTick 3,之后才是setImmediate 2,之后就是microTask里的setInterval间隔1ms不停输出了

浏览器

浏览器的实现跟Node不一致,所以可能结果有前后区别。

async function async1() {
    console.log("a");
    const res = await async2();
    console.log("b");
}

async function async2() {
    console.log("c");
    return 2;
}

console.log("d");

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

async1().then(res => {
    console.log("f")
})

new Promise((resolve) => {
    console.log("g");
    resolve();
}).then(() => {
    console.log("h");
});

console.log("i");

/**
* 输出结果:d a c g i b h f e 
*/
  1. 扫代码输出12行console的d,然后遇到14行setTimeout推入macroTask(【macroTask有setTimeout,microTask为空】)
  2. 之后18行async1执行输出第2行的a,执行asycn2输出第8行的c,同时第9行async2返回2,但是被第3行的await阻塞,后面的赋值和console.log等等被阻塞,等待执行(【macroTask有setTimeout,microTask有第3行await阻塞的】)
  3. 继续到22行Promise构造函数,输出g后25行Promise.then加入microTask(【macroTask有setTimeout,microTask有第3行await阻塞的和25行的Promise.then】)
  4. 继续扫script输出i
  5. 要清空microTask,找到最先加入的,就是第3行的await阻塞的,继续执行第三行res赋值为2,然后输出b,async1执行完毕,到18行的Promise.then,加入microTask(【macroTask有setTimeout,microTask有25行的Promise.then和18行的Promise.then】)
  6. 取microTask的25行的Promise.then执行,输出h,再取18行的Promise.then,输出f(【macroTask有setTimeout)
  7. microTask清空,继续macroTask执行,取setTimeout执行,输出e,全部结束

参考

Last Updated:
<manfred>峯</hu>
欢迎关注微信公众号 【Big前端】无广告,无软文,就是这么傲娇。直推一线大厂高质量内容,不局限于前端·后台·运维相关,还包括房价🏠、信用卡💳等内容也可内推一线大厂腾讯阿里字节,对腾讯字节比较熟悉,简历可以发给我,我会给你介绍一线大厂的情况,让你更加了解一线大厂