十--nodejs原理(事件循环)

一、事件循环
什么是事件循环?
事件循环使得nodejs可以通过将操作转移到系统内核中来执行非阻塞I/O操作(尽管javascripts是单线程的)。由于大多数现代内核是多线程的,因此它们可以处理在后台执行的多个操作。当这些操作之一完成时,内核会告诉nodejs,以便可以将适度的回调添加到轮询队列中以最终执行
通俗的说在nodejs内部使用了第三方库libuv,nodejs会把IO,文件读取等异步操作交由他处理,而nodejs主线程可以继续去处理其他的事情。libuv会开启不同的线程去处理这些延时操作,处理完后,会把异步操作的回调函数放到nodejs的轮询队列中,nodejs会在适当的时候处理轮询队列中的回调函数,从而实现非阻塞。
nodejs启动的时候,它将初始化事件循环,处理提供的输入脚本,这些脚本可能会进行异步api调用,调度计时器或调用process.nextTick,然后开始处理事件循环。
以下是事件循环模型
在这里插入图片描述
每个阶段都有一个要执行的回调 FIFO 队列。 尽管每个阶段都有其自己的特殊方式,但是通常,当事件循环进入给定阶段时,它将执行该阶段特定的任何操作,然后在该阶段的队列中执行回调,直到队列耗尽或执行回调的最大数量为止。 当队列已为空或达到回调限制时,事件循环将移至下一个阶段,依此类推。

  1. timers:此阶段执行由 setTimeout 和 setInterval 设置的回调。
  2. pending callbacks:执行推迟到下一个循环迭代的 I/O 回调。
  3. idle, prepare, :仅在内部使用。
  4. poll:取出新完成的 I/O 事件;执行与 I/O 相关的回调(除了关闭回调,计时器调度的回调和 setImmediate 之外,几乎所有这些回调) 适当时,node 将在此处阻塞。
  5. check:在这里调用 setImmediate 回调。
  6. close callbacks:一些关闭回调,例如 socket.on(‘close’, …)。

在每次事件循环运行之间,Node.js 会检查它是否正在等待任何异步 I/O 或 timers,如果没有,则将其干净地关闭。
1、timer阶段
计时器可以在回调后面指定时间阈值,但这不是我们希望其执行的确切时间。 计时器回调将在经过指定的时间后尽早运行。 但是,操作系统调度或其他回调的运行可能会延迟它们。-- 执行的实际时间不确定
例如:

const fs = require('fs');
function someAsyncOperation(callback){
  //假设它执行花了95ms
  fs.readFile('/path/to/file',callback);
}
const timeoutScheduled = Date.now();
setTimeout(()=>{
  const delay = Date.now()-timeoutScheduled;
  console.log(`${delay}ms have passed since i was scheduled`)
},100)
someAsyncOperation(()=>{
  const startCallback = Date.now();
  //这里在读取文件之后执行了一个10秒的循环
  //那么能执行定时器回调就一定是在105秒之后
  while(Date.now() - startCallback<10){
    //do nothing
  }
})

当事件循环进入 poll 阶段时,它有一个空队列(fs.readFile 尚未完成),因此它将等待直到达到最快的计时器 timer 阈值为止。
等待 95 ms 过去时,fs.readFile 完成读取文件,并将需要 10ms 完成的其回调添加到轮询 (poll) 队列并执行。
回调完成后,队列中不再有回调,此时事件循环已达到最早计时器 (timer) 的阈值 (100ms),然后返回到计时器 (timer) 阶段以执行计时器的回调。
在此示例中,您将看到计划的计时器与执行的回调之间的总延迟为 105ms。
2、pending callback
此阶段执行某些系统操作的回调,例如 TCP 错误。 平时无需关注
3、轮询poll阶段
主要有两个功能:
1)、计算应该阻塞并I/O轮询的时间
2)、处理轮训队列中的(poll queue)中的事件
当事件循环进入轮询 (poll) 阶段并且没有任何计时器调度 (timers scheduled) 时,将发生以下两种情况之一:
1)如果轮询队列 (poll queue) 不为空,则事件循环将遍历其回调队列,使其同步执行,直到队列用尽或达到与系统相关的硬限制为止 (到底是哪些硬限制?)。
2)如果轮询队列为空,则会发生以下两种情况之一:
2.1 如果已通过 setImmediate 调度了脚本,则事件循环将结束轮询 poll 阶段,并继续执行 check 阶段以执行那些调度的脚本。
2.2 如果脚本并没有 setImmediate 设置回调,则事件循环将等待 poll 队列中的回调,然后立即执行它们。
一旦轮询队列 (poll queue) 为空,事件循环将检查哪些计时器 timer 已经到时间。 如果一个或多个计时器 timer 准备就绪,则事件循环马上结束这一阶段往下一阶段执行,直到返回到计时器阶段,以执行这些计时器的回调。
4、检查阶段check
此阶段允许在轮询 poll 阶段完成后立即执行回调。 如果轮询 poll 阶段处于空闲,并且脚本已使用 setImmediate 进入 check 队列,则事件循环可能会进入 check 阶段,而不是在 poll 阶段等待。
setImmediate 实际上是一个特殊的计时器,它在事件循环的单独阶段运行。 它使用 libuv API,该 API 计划在轮询阶段完成后执行回调。
通常,在执行代码时,事件循环最终将到达轮询 poll 阶段,在该阶段它将等待传入的连接,请求等。但是,如果已使用 setImmediate 设置回调并且轮询阶段变为空闲,则它将将结束并进入 check 阶段,而不是等待轮询事件。
5、close callbacks 阶段
如果套接字或句柄突然关闭(例如 socket.destroy),则在此阶段将发出 ‘close’ 事件。 否则它将通过 process.nextTick 发出。
二、事件循环相关问题
1、setImmediate 和 setTimeout 的区别
setImmediate 和 setTimeout 相似,但是根据调用时间的不同,它们的行为也不同。
1)setImmediate设计为当前poll阶段完成后执行脚本
2)setTimeout计划在以毫秒为单位的最小阈值过去之后执行脚本
Tips: 计时器的执行顺序将根据调用它们的上下文而有所不同。 如果两者都是主模块中调用的,则时序将受到进程性能的限制。
(1)在主模块中执行----宏任务代码
两者的执行顺序是不固定的, 可能timeout在前, 也可能immediate在前。

setTimeout(()=>{
  console.log('timeout');
},0)
setImmediate(()=>{
  console.log('immediate')
})

(2)在同一个I/O回调里执行,或timers回调里
setImmediate总是先执行

const fs = require('fs');
fs.readFile(__filename,()=>{
  setTimeout(()=>{
    console.log('timeout');
  },0)
  setImmediate(()=>{
    console.log('immediate')
  })
})

那为什么在外部 (比如主代码部分 mainline) 这两者的执行顺序不确定呢?
因为在 主代码 部分执行 setTimeout 设置定时器 (此时还没有写入队列),与 setImmediate 写入 check 队列。
mainline 执行完开始事件循环,第一阶段是 timers,这时候 timers 队列可能为空,也可能有回调;
如果没有那么执行 check 队列的回调,下一轮循环在检查并执行 timers 队列的回调;
如果有就先执行 timers 的回调,再执行 check 阶段的回调。因此这是 timers 的不确定性导致的。
那么在timer回调里执行settimeout和setimmediate,setimmediate先执行。一样的道理,在timer回调里执行代码,定时器不会马上写入队列。那么timer回调执行完下一个阶段就是poll和check阶段,先执行immedaite。

setTimeout(()=>{
  setTimeout(() => {
  console.log('timeout');
  }, 0);
  setImmediate(() => {
  console.log('immediate');
  });
  new Promise(function (resolve) {
        console.log('promise1')
        resolve();
        console.log('promise2')
    }).then(function () {
        console.log('promise3')
    })
},0)
setTimeout(()=>{
  new Promise(function (resolve) {
    console.log('promise11')
    resolve();
    console.log('promise21')
  }).then(function () {
      console.log('promise31')
  })
  setTimeout(() => {
    console.log('timeout11');
    }, 0);
},0)

我们看以上代码的输出是:
promise1
promise2
promise3
promise11
promise21
promise31
immediate
timeout
timeout11
我的理解promise11比pomise3还迟输出,那意味着第一个settimeout队列回调完成先执行自己的微任务。在主线结束后以及事件循环的每个阶段之后,立即运行微任务回调。所以timer一个回调结束后会执行微任务回调。
注意
node10版本以前:
1)、执行完一个阶段的所有任务;
2)、执行完nextick队列里面的内容;
3)、然后执行微任务队列里的内容。
node11之后:
1)和浏览器统一,都是每执行完一个宏任务就去执行微任务队列。
所以这也能解释以上promise11在promise3之后输出,且immediate在最后输出。顺序是timer阶段的一个宏任务执行完就去执行微任务,同一时刻timer有两个宏任务,上一个宏任务的微任务执行完继续执行宏任务,接着执行微任务,执行完timer的回调队列为空就去执行下一阶段的check阶段即immediate,然后在执行后面的宏任务。
2、process.nextTick
process.nextTick 从技术上讲不是事件循环的一部分。 相反,无论事件循环的当前阶段如何,都将在当前操作完成之后处理 nextTickQueue。
在这里插入图片描述
3、process.nextTick 和 setImmediate 的区别
1)process.nextTick 在同一阶段立即触发
2)setImmediate fires on the following iteration or ‘tick’ of the event loop (在事件循环接下来的阶段迭代中执行 - check 阶段)。
注意因为process.nextTick会在事件轮询每个阶段之间执行, 如果递归调用nextTick, 就会导致轮询阻塞,所以尽量避免使用process.nextTick, 可以使用setImmediate代替。
4、microtasks微任务
在 Node 领域,微任务是来自以下对象的回调:

  1. process.nextTick()
  2. then()
    主线结束后以及事件循环的每个阶段之后,立即运行微任务回调。
    resolved 的 promise.then 回调像微任务处理一样执行是放入microTaskQueue队列中,就像 process.nextTick 一样。 虽然,如果两者都在同一个微任务队列中,则将首先执行 process.nextTick 的回调。
    优先级 process.nextTick > promise.then
    5、代码执行
async function async1() {
    console.log('async1 start')
    await async2()//这个就相当于是async2().then(()=>{console.log('async1 end')}
    console.log('async1 end')//所以这个被放入微任务里
}
async function async2() {
    console.log('async2')
}
console.log('script start')
setTimeout(function () {
    console.log('setTimeout0')
    setTimeout(function () {
        console.log('setTimeout1');
    }, 0);
    setImmediate(() => console.log('setImmediate'));
}, 0)

process.nextTick(() => console.log('nextTick'));//在同一轮微任务中,它优先级最高
async1();
new Promise(function (resolve) {
    console.log('promise1')
    resolve();
    console.log('promise2')
}).then(function () {
    console.log('promise3')//这个被放入微任务里
})
console.log('script end')

输出顺序:
script start
async1 start
async2
promise1
promise2
script end
//开始微任务回调
nextTick
async1 end
promise3
//settimeout timer回调
setTimeout0
//同一个i/o和timers里setImmediate比setTimeout先执行
setImmediate
setTimeout1

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值