Nodejs中的Event Loop事件循环

Nodejs中的事件循环

    • 事件循环的概念
    • 事件循环的6个阶段
      • demo
    • setTimeout和setImmediate
      • 定时器可能是不准的
    • 特殊的process.nextTick
      • nextTick应用场景
    • 参考

事件循环的概念

提及事件循环(Event Loop), 想必大家第一反映就是浏览器中的事件循环、宏任务、微任务、任务队列等概念。这些设计就是用来解决js单线程带来的异步问题。不清楚的小伙伴, 可以翻阅现代JavaScript教程中的相关介绍, 个人感觉讲的还是很不错的。
但是今天想和大家分享的Nodejs事件循环, 和浏览器的事件循环完全不一样

事件循环的6个阶段

Nodejs中的Event Loop事件循环_第1张图片
上图就是一轮事件循环所要经历的六个阶段。 需要注意的是,这幅图只是各阶段的排序图, 但是实际事件循环在执行回调时,会因回调的触发时机不同,导致执行顺序不同。

  1. timers (定时器)
    此阶段执行那些由 setTimeout()setInterval() 调度的回调函数.
  2. I/O callbacks (I/O回调)
    此阶段会执行几乎所有的回调函数, 除了 close callbacks(关闭回调) 和 那些由 timers 与 setImmediate()调度的回调.
  3. idle(空转), prepare
    仅系统内部使用。
  4. poll(轮询)
    检索新的I/O事件; 在恰当的时候Node会阻塞在这个阶段
  5. check(检查)
    setImmediate() 设置的回调会在此阶段被调用
  6. close callbacks(关闭事件的回调)
    诸如 socket.on(‘close’, …) 此类的回调在此阶段被调用

在事件循环的每次运行之间, Node.js会检查它是否在等待异步I/O或定时器, 如果没有的话就会自动关闭.

如果event loop进入了 poll阶段,且代码未设定timer,将会发生下面情况:

  • 如果poll queue不为空,event loop将同步的执行queue里的callback,直至queue为空,或执行的callback到达系统上限;
  • 如果poll queue为空,将会发生下面情况:
    • 如果代码已经被setImmediate()设定了callback, event loop将结束poll阶段进入check阶段,并执行check阶段的queue (check阶段的queue是 setImmediate设定的)
    • 如果代码没有设定setImmediate(callback),event loop将阻塞在该阶段等待callbacks加入poll queue,一旦到达就立即执行

如果event loop进入了 poll阶段,且代码设定了timer:

  • 如果poll queue进入空状态时(即poll 阶段为空闲状态),event loop将检查timers,如果有1个或多个timers时间时间已经到达,event loop将按循环顺序进入 timers 阶段,并执行timer queue.

是不是看了有点上头?没关系, 我来画一张图:
我们以poll阶段为当前参考对象,把事件循环想象成排队打疫苗,那么现在这个队伍就有3个主要参与者(其他暂时忽略):1. timers, 2.poll, 3. check。
假设排在第一个的timers迟到了,还没有来(代码未设定timers,即定时器内的回调事件还没到, 或没有设置定时器)

  1. 在轮到poll打疫苗的时候, 医生会先看看poll到底来了没有(是否为空闲状态),如果poll到了,那就给poll打。
  2. 但是如果poll也迟到了, 此时这个位置就空着了(poll queue为空)那么,医生会先回头看看timers有没有来(timers里有没有放入定时器内的回调事件),如果timers此时来了, 那会让timers先打疫苗, 毕竟人家是先挂号的(事件循环绕回到timers阶段)。
  3. 如果poll、timers都还没来(poll queue为空,且代码未设定timer)那么医生就要看看后面的check来了没有,如果check来了(代码已经被setImmediate()设定了callback),那就立刻给check打疫苗(立即进入check阶段, 执行setImmediate的回调)
  4. 但是如果check也没有来,那么意味着没有人来排队打疫苗, 那么医生会在poll阶段一直等待,这就是所谓的 “在恰当的时候Node会阻塞在这个阶段”。
    Nodejs中的Event Loop事件循环_第2张图片

demo

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

const timeoutScheduled = Date.now();
function someAsyncOperation (callback) {
  // 因为fs读取文件的速度很快(大概1.几毫秒), 所以我们这里假设当前读取文件需要2毫秒
  fs.readFile(path.resolve(__dirname, './read.txt'), callback);
}

setTimeout(function () {
  const delay = Date.now() - timeoutScheduled;
  console.log('setTimeout');
}, 10);

someAsyncOperation((err, data) => {
  fileReadtime = Date.now();
  // 通过while强行阻塞20ms, 那么从读取到执行回调 总共消耗2-22ms
  while(Date.now() - fileReadtime < 20) {
	console.log('readFile');
  }
});

setImmediate(() => {
    console.log('check阶段')
})

打印结果
‘check阶段’
readFile

readFile
setTimeout


执行过程:
1 执行setTimeout, (将在10ms后将回调放入timers)
2 事件循环进入到poll阶段,开始不断的轮询监听事件
3 poll阶段 执行someAsyncOperation事件, 开始读取文件(2ms后执行回调)
4 执行setImmediate, 结束poll阶段, 进入check阶段, 打印"check阶段"
5 第一轮事件循环结束, 开始第二轮事件循环
6 因为时间仍未到定时器截止时间(现在过去2ms),所以事件循环又一次进入到poll阶段,进行轮询
7 poll阶段 队列里有fs的回调, 所以打印"readFile … readFile" 一直阻塞到20ms后结束, (在这过程中, 10ms的setTimeout回调时无法执行的)。
8 第二次事件循环结束, 进入到下一轮事件循环,此时发现timer事件队列已经添加了setTimeout的回调,所以开始打印"setTimeout"

当然, 如果setTimeout时间阈值改为2ms, 读取文件的事件改为10ms, 那么结果就是:

‘check阶段’
‘setTimeout’
‘readFile … readFile’

setTimeout和setImmediate

一个很有意思的demo

setTimeout(function timeout () {
  console.log('timeout');
}, 0);

setImmediate(function immediate () {
  console.log('immediate');
});

定时器可能是不准的

在nodejs中, 你会发现 他们的结果是不确定的, 会有概率出现setImmediate先执行的情况, 这是因为:
node.js里面setTimeout(fn, 0)会被强制改为setTimeout(fn, 1),这在官方文档中有说明。如果同步代码执行时间较长,进入Event Loop的时候1毫秒已经过了,setTimeout执行,如果1毫秒还没到,就先执行了setImmediate。在浏览器中一样存在这个问题, 即setTimeout最小时间阈值为4ms

特殊的process.nextTick

process.nextTick()不在event loop的任何阶段执行,而是在各个阶段切换的中间执行,即从一个阶段切换到下个阶段前执行。

const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('setTimeout');
  }, 0);
  setImmediate(() => {
    console.log('setImmediate');
    process.nextTick(()=>{
      console.log('nextTick3');
    })
  });
  process.nextTick(()=>{
    console.log('nextTick1');
  })
  process.nextTick(()=>{
    console.log('nextTick2');
  })
});

第一次timers为空 进入轮询阶段(poll 为空), 此时执行setImmediate 进入check阶段, 但是在事件循环切换阶段会执行process.nextTick所以, 会先打印 ‘nextTick1’ 和 ‘nextTick2’
check阶段, 1. 执行’setImmediate’, 2. 遇到异步的process.nextTick (放入i/o线程)>第二次event loop:
从上一次check阶段 进入第二次事件循环的timers阶段, 但是此时会在切换时执行process.nextTick, 所以, 依然会先打印 ‘nextTick3’
再执行setTimeout
结果:
nextTick1
nextTick2
setImmediate
nextTick3
setTimeout

nextTick应用场景

  1. 在多个事件里交叉执行CPU运算密集型的任务:
    const http = require('http');
    function compute() {
        process.nextTick(compute);
    }
    http.createServer(function(req, res) {  // 服务http请求的时候,还能抽空进行一些计算任务
         res.writeHead(200, {'Content-Type': 'text/plain'});
         res.end('Hello World');
    }).listen(5000, '127.0.0.1');
    compute();

在这种模式下,我们不需要递归的调用compute(),我们只需要在事件循环中使用process.nextTick()定义compute()在下一个时间点执行即可。在这个过程中,如果有新的http请求进来,事件循环机制会先处理新的请求,然后再调用compute()。反之,如果你把compute()放在一个递归调用里,那系统就会一直阻塞在compute()里,无法处理新的http请求了。

  1. 保持回调函数异步执行的原则

当你给一个函数定义一个回调函数时,你要确保这个回调是被异步执行的。下面我们看一个例子,例子中的回调违反了这一原则:

    function asyncFake(data, callback) {        
        if(data === 'foo') callback(true);
        else callback(false);
    }
    asyncFake('bar', function(result) {
        // this callback is actually called synchronously!
    });

为什么这样不好呢?我们来看Node.js 文档里一段代码:

var client = net.connect(8124, function() { 
    console.log('client connected');
    client.write('world!\r\n');
});

在上面的代码里,如果因为某种原因,net.connect()变成同步执行的了,回调函数就会被立刻执行,因此回调函数写到客户端的变量就永远不会被初始化了。

这种情况下我们就可以使用process.nextTick()把上面asyncFake()改成异步执行的:

function asyncReal(data, callback) {
    process.nextTick(function() {
        callback(data === 'foo');       
    });
}
  1. 用在事件触发过程中
    EventEmitter有2个比较核心的方法, on和emit。node自带发布/订阅模式
    var EventEmitter = require('events').EventEmitter;
    function StreamLibrary(resourceName) { 
        this.emit('start');
    }
    StreamLibrary.prototype.__proto__ = EventEmitter.prototype;   // inherit from EventEmitter
    const stream = new StreamLibrary('fooResource');
    stream.on('start', function() {
        console.log('Reading has started');
    });
    function StreamLibrary(resourceName) {      
        var self = this;
        process.nextTick(function() {
            self.emit('start');
        });  // 保证订阅永远在发布之前
        // read from the file, and for every chunk read, do:        
    }

这次分享就先到这里,感谢阅读!

参考

  • 事件循环:微任务和宏任务
  • Node.js 事件循环,定时器和 process.nextTick()
  • 深入理解NodeJS事件循环机制
  • setTimeout和setImmediate到底谁先执行,本文让你彻底理解Event Loop
  • setTimeout(callback[, delay[, …args]])
  • 实际延时比设定值更久的原因:最小延迟时间

你可能感兴趣的:(nodejs,JavaScript,javascript,大前端,node.js)