Node.js事件循环

先给大家提个问题,下面的执行结果是什么呢?

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
(() => console.log(5))();

这儿先卖个关子,不提结果,大家接着往下看。

事件循环(event loop)

node.js是基于事件循环的事件模型。
当node.js启动时,它会初始化事件循环,处理所提供的输入脚本,该脚本可以进行Asynsynapi调用、调度计时器或调用process.nexttick-lrb-rrb-,然后开始处理事件循环。
只有一个主线程,事件循环是在主线程上完成的,基于此node.js实现了单线程高效的异步IO。
Node开始执行脚本时,会先进行事件循环的初始化,但是这时事件循环还没有开始,会先完成下面的事情。

同步任务
发出异步请求
规划定时器生效的时间
执行process.nextTick()等等
最后,上面这些事情都干完了,事件循环就正式开始了。

事件循环的六个阶段
事件循环会无限次地执行,一轮又一轮。只有异步任务的回调函数队列清空了,才会停止执行。
每一轮的事件循环,分成六个阶段,这些阶段会依次执行。

1.timers
2.I/O callbacks
3.idle,prepare
4.poll
5.check
6.close callbacks

每个阶段都有一个先进先出的回调函数队列。只有一个阶段的回调函数队列清空了,该执行的回调函数都执行了,事件循环才会进入下一个阶段。

(1)timers
    这个是定时器阶段,处理setTimeout()和setInterval()的回调函数。进入这个阶段后,主线程会检查一下当前时间,是否满足定时器的条件。如果满足就执行回调函数,否则就离开这个阶段。
(2)I/O callbacks
    除了以下操作的回调函数,其他的回调函数都在这个阶段执行
    ~setTimeout()和setInterval()的回调函数
    ~setImmediate()的回调函数
    ~用于关闭请求的回调函数,比如socket.on('close',...)
(3)idle,prepare
    该阶段只供libuv内部调用
(4)Poll
    这个阶段是轮询时间,用于等待还未返回的I/O事件,比如服务器的回应、用户移动鼠标等等。
    这个阶段的时间会比较长,如果没有其他异步任务要处理(比如到期的定时器),会一直停留在这个阶段,等待I/O请求返回结果。
(5)check
    该阶段执行setImmediate()的回调函数
(6)close callbacks
    该阶断执行关闭请求的回调函数,比如socket.on('close',...)

看下面这个事例:

const fs = require("fs");
const timeoutScheduled = Date.now();
//异步任务一:100ms后执行的定时器
setTimeout(() => {
    const delay = Date.now() - timeoutScheduled;
    console.log(`${delay}ms`)
},100);
//异步任务二:文件读取后,执行一个用时200ms的回调函数
fs.readFile('test.js',() => {
    const startCallback = Date.now();
    while (Date.now() - startCallback < 200) {
        //什么也不做
    }
})

脚本进入第一轮事件循环以后,没有到期的定时器,也没有已经可以执行的I/O回调函数,所以会进入Poll阶段,等待内核返回文件读取的结果。由于读取小文件一般不会超过100ms,所以在定时器到期之前,Poll阶段就会得到结果,因此就会继续往下执行。

第二轮事件循环,依然没有到期的定时器,但是已经有了可以执行的I/O回调函数,所以会进入I/O callbacks阶段,执行fs.readFile的回调函数。这个回调函数需要200ms,也就是说,在它执行到一半的时候,100ms的定时器就会到期。但是必须等到这个回调函数执行完,才会离开这个阶段

第三轮事件循环,已经有了到期的定时器,所以会在timers阶段执行定时器。最后输出结果哦大概是200多毫秒。
setTimeout和setImmediate
由于setTimeout在timers阶段执行,而setImmediate在check阶段执行。所以,setTimeout会早于setImmediate完成

setTimeout(() => console.log(1));
setImmediate(() => console.log(2));

上面代码应该先输出1,再输出2,但是实际执行的时候,结果却是不确定,有时还会先输出2,再输出1。

这是因为setTimeout的第二个参数默认为0。但是实际上,NODE做不到0毫秒,最少也需要1毫秒。根据官方文档,第二个参数的取之范围在1毫秒到2147483647毫秒之间。也就是说,setTimeout(f,0)等同于setTimeout(f,1)。

实际执行的时候,进入事件循环以后,有可能到了1毫秒,也可能还没到1毫秒,取决于系统当时的状况。如果没到1毫秒,那么timers阶段就会跳过,进入check阶段,先执行setImmediate的回调函数。

const fs = require('fs')
fs.readFile('test.js',() => {
    setTimeout(() => console.log(1));
    setImmediate(() => console.log(2));
})
//2 1

上面代码会先进入I/O callbacks阶段,然后是check阶段,最后才是timers阶段。因此,setImmediate才会早于setTimeout执行。

setImmediate(function(){
    console.log(1);
    process.nextTick(function(){
        console.log(2);
    });
});
process.nextTick(function(){
    console.log(3);
    setImmediate(function(){
        console.log(4);
    })
});
//3 1 4 2

两个setImmediate在同一轮循环的同一个队列里面。只有清空了这个队列,才会进入下一个阶段。

process.nextTick(() => console.log(1));
Promise.resolve().then(() => console.log(2));
process.nextTick(() => console.log(3));
Promise.resolve().then(() => console.log(4));
//1 3 2 4

Node规定,process.nextTick和Promise的回调函数,追加在本轮循环,即同步任务一旦执行完成,就开始执行它们。而setTimeout、setInterval、setImmediate的回调函数,追加在次轮循环。
process.nextTick是所有异步任务里面最快执行的。如果希望异步任务尽可能快地执行,那就使用它。
根据语言规格,Promise对象的回调函数,会进入异步任务里面的“微任务”(microtask)队列。微任务队列会追加在nextTick队列之后。且只有前一个队列全部清空以后,才会执行下一个队列。

看到这儿,想必大家对node中事件的执行顺序有了了解了吧,也可以轻易的解决最上方的问题了吧。(5 3 4 1 2)

总结一下:
一、同步任务和异步任务
同步任务总是比异步任务更早执行
二、本轮循环和次轮循环
追加在本轮循环的异步任务
追加在次轮循环的异步任务
本轮循环一定早于次轮循环执行
node事件执行顺序:
1.同步任务
2.process.nextTick()
3.微任务

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值