node事件循环中事件执行顺序

Posted 六卿

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了node事件循环中事件执行顺序相关的知识,希望对你有一定的参考价值。


事件循环

在浏览器环境下我们的js有一套自己的事件循环,同样在node环境下也有一套类似的事件循环。



浏览器环境事件循环

首先,我们先来回顾一下在浏览器的事件循环:

总结来说:

首先会运行主线程的同步代码,每一行同步代码都会被压入执行栈,每一行异步代码会压入异步API中(如:定时器线程、ajax线程等;),在执行栈没有要执行的代码时,也就是我们当前主线程没有同步代码了,任务队列会从我们的异步任务微任务队列中取一个微任务放到我们的任务队列中进行执行,将它的回调函数进而再次放到执行栈中进行执行,当微任务队列为空时,会在宏任务中取异步任务加到任务队列,进而压入执行栈,执行回调函数,然后继续在该宏任务中查找同步、异步任务,一次循环,完成了一个事件循环(事件轮询)

浏览器环境下的例子:

例子:

        console.log("1");
        setTimeout(() => {
            console.log("setTimeout");
        }, 1);
        new Promise((res, rej) => {
            console.log("Promise");
            res('PromiseRes')
        }).then(val => {
            console.log(val);
        })
        console.log("2");

分析:
首先执行栈找到第一行的同步代码,直接扔到执行栈中执行,打印1,随后为定时器setTimeout,为异步任务,将代码放到异步对列中等待执行,随后执行promise中的代码,我们要清楚promise是同步执行,它的回调是异步执行,所有打印Promise,将res(‘PromiseRes’)放到异步对列中等待执行,这个时候又遇到了同步代码,打印2,当前主线程的同步代码全部执行完毕,并且执行栈中没有要执行的同步代码,这个时候webApi会从异步队列中去微任务队列中的第一个,加入到事件队列执行,将返回的回调函数压入到执行栈中执行,打印PromiseRes,随后微任务执行完毕,已经没有微任务,现在就需要从宏任务队列中取宏任务定时器,加入到任务队列中,将回调函数压入到执行栈中执行,打印setTimeout。

node环境事件循环

在node中事件循环主要分为六个阶段来实现:

外部数据输入–》轮询阶段–》检查阶段–》关闭事件回调阶段–》定时器阶段–》I/O回调阶段–》闲置阶段–》轮询阶段》…开始循环

六个阶段

图片来自网络

timers阶段:用来执行timer(setTimeout,setInterval)的回调;
I/O callbacks阶段:处理一些上一轮循环中少数未执行的I/O回调
idle,prepare 阶段:仅node内部使用,我们用不到;
poll阶段:获取新的I/O时间,适当的条件下node将阻塞在这里;
check阶段:执行setImmediate()的回调;
close callbacks 阶段:执行socket的close时间回调

主要阶段
timer:
timers阶段会执行setTimeout和setInterval回调,并且是由poll阶段控制的。
同样,在node中定时器指定的时间也不是准确时间,只能是尽快执行。
poll:
poll这一阶段中,系统会做两件事情:
1.回到timer阶段执行回调
2.执行I/O回调
并且在进入该阶段时如果没有设定了timer 的话,会发生以下两件事情

如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者达到系统限制
如果 poll 队列为空时,会有两件事发生
1、如果有 setImmediate 回调需要执行,poll 阶段会停止并且进入到 check 阶段执行回调
2、如果没有 setImmediate 回调需要执行,会等待回调被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去
当然设定了 timer 的话且 poll 队列为空,则会判断是否有 timer 超时,如果有的话会回到 timer 阶段执行回调。

check阶段
setImmediate()的回调会被加入 check 队列中,从 event loop 的阶段图可以知道,check 阶段的执行顺序在 poll 阶段之后,在进入check阶段执勤poll会检查有的话到check阶段,没有的换直接到timer阶段。

(1) setTimeout 和 setImmediate

二者非常相似,区别主要在于调用时机不同。

setImmediate 设计在 poll 阶段完成时执行,即 check 阶段,只有在check阶段才会执行;
setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行,表示当前线程没有其他可执行的同步任务,才会在timer阶段执行定时器。

这两个执行的时机可前可后:
例子1:

// //异步任务中的宏任务
setTimeout(() => {
    console.log('===setTimeout===');
},0);
setImmediate(() => {
    console.log('===setImmediate===')
})

多次重复执行的结果会不同,有一种随机的感觉,出现这种情况的原因主要和setTimeout的实现代码有关,当我们不传时间参数或者设置为0的时候,nodejs会取值为1,即1ms(在浏览器端可能取值会更大一下,不同浏览器也各不相同),所以在电脑cpu性能够强,能够在1ms内执行到timers phase的情况下,由于时间延迟不满足回调不会被执行,于是只能等到第二轮再执行,这样setInterval就会先执行。
可能由于cpu多次执行相同任务用时会有细微差别,而且在1ms上下浮动,才会造成上面的随机现象
一般情况下setTimeout为0时候会在setImmediate之前执行

例子2:
当我们传入的值大于定时器timer执行的回调时间的时候会直接导致定时器在下一次事件循环中执行

setTimeout(() => {
    console.log('===setTimeout===');
},10);
setImmediate(() => {
    console.log('===setImmediate===')
})


例子3:
当我们将上述代码放入一个i/o中就会固定先check再而timer:


const fs = require('fs');

fs.readFile("./any.js", (data) => {
    setTimeout(() => {
        console.log('===setTimeout===');
    },10);
    setImmediate(() => {
        console.log('===setImmediate===')
    })
});


在第一轮循环中读取文件,在回调中,会进入check阶段进而执行setImmediate,随后timer阶段执行定时器。
setimmediate 与 settimeout 放入一个 I/O 循环内调用,则 setImmediate 总是被优先调用

(2) process.nextTick

这个函数其实是独立于 Event Loop 之外的,它有一个自己的队列,当每个阶段完成后,如果存在 nextTick 队列,就会清空队列中的所有回调函数,并且优先于其他 microtask 执行。

例子1:

setTimeout(() => {
 console.log('timer1')
 Promise.resolve().then(function() {
   console.log('promise1')
 })
}, 0)
process.nextTick(() => {
 console.log('nextTick')
 process.nextTick(() => {
   console.log('nextTick')
   process.nextTick(() => {
     console.log('nextTick')
     process.nextTick(() => {
       console.log('nextTick')
     })
   })
 })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

例子2:


const fs = require('fs');

fs.readFile("./any.js", (data) => {
    process.nextTick(()=>console.log('process===2'))
    setTimeout(() => {
        console.log('===setTimeout===');
    },10);
    setImmediate(() => {
        console.log('===setImmediate===')
    })
});
process.nextTick(()=>console.log('process===1'))

练习例子

async function async1() {
    console.log('2')
    //会等待await执行完 但是不会向下执行 因为下面输入微任务
    await async2()
    console.log('9')
  }
   
   function async2() {
    console.log('3')
  }
   
  console.log('1')
   
  setTimeout(function () {
    console.log('11')
  }, 0)
   
  setTimeout(function () {
    console.log('13')
  }, 300)
   
  setImmediate(() => console.log('12'));
   
  process.nextTick(() => console.log('7'));
   
  async1();
   
  process.nextTick(() => console.log('8'));
   
  new Promise(function (resolve) {
    console.log('4')
    resolve();
    console.log('5')
  }).then(function () {
    console.log('10')
  })
   
  console.log('6')

分析:
上面的循序就是序号的顺序;
首先打印1:
前面都是两个函数声明,所有直接打印1,这行同步代码;
打印2:
打印完1后,都是异步代码,加入异步任务队列,直接到async1函数调用,在这个函数中打印2;
打印3:
async1这个函数是个async await函数,所有也是一个变相的同步操纵等待async2函数执行,async2执行后并不会直接打印9,原因await接受的是一个promise的then操作,所以后面属于一个promise的回调操作属于微任务,加入微任务队列;
打印4:
process.nextTick为微任务,所以会继续执行promise,打印4;
打印5:
resolve()的回调不会立即执行属于微任务,加入微任务队列,所以打印5;
打印6:
最后一个主线程的同步代码,打印6;
打印7、8:
process.nextTick优先级高于其他定时器,所以会直接执行回调函数打印7、8;
打印9、10:
这个时候需要执行微任务队列中的微任务,目前有两个9和10,按照先后循序,先打印9后打印10;
打印11、12:
setTimeout为0秒比setImmediate执行早,按照先后循序,先打印11后打印12;
打印13:
setTimeout为300ms的函数,打印13;

例子:

async function async1() {
    console.log('2')
    //会等待await执行完 但是不会向下执行 因为下面输入微任务
    await async2()
    console.log('9')
  }
   
   function async2() {
    console.log('3')
  }
   
  console.log('1')
   
  setTimeout(function () {
    console.log('11')
    setTimeout(() => {
        console.log('11-1');
    },100);
    setImmediate(() => {
        console.log('11-2')
    })
  }, 0)
   
  setTimeout(function () {
    console.log('13')
    setTimeout(() => {
        console.log('15');
    },10);
    setImmediate(() => {
        console.log('14')
    })
  }, 300)
  setImmediate(() => console.log('12'));
  process.nextTick(() => console.log('7'));
  async1();
   
  process.nextTick(() => console.log('8'));
   
  new Promise(function (resolve) {
    console.log('4')
    resolve();
    console.log('5')
  }).then(function () {
    console.log('10')
  })
   
  console.log('6')

总结:

理解不对的地方,还请各位大佬给予指正。

参考:https://www.cnblogs.com/everlose/p/12846375.html

以上是关于node事件循环中事件执行顺序的主要内容,如果未能解决你的问题,请参考以下文章

初学Node事件循环

浏览器和Node的事件循环

浏览器和Node的事件循环

浏览器事件循环与node事件循环

js事件循环运行机制

js事件循环运行机制