浅谈Node.js中的事件循环机制

1. 循环原理

node和js的事件循环原理有所不同,即使在node 10+版本后二者的执行顺序一致。node基于libev库,js基于浏览器。js事件循环核心是宏任务和微任务,而node除此之外还有现阶段任务执行阶段

浅谈Node.js中的事件循环机制_第1张图片

  1. timers:本阶段执行setInterval和setTimeout的回调函数
  2. pending callbacks:执行某些系统操作(非node)的回调函数
  3. idle,prepare:仅系统内部调用
  4. poll:检索新的i/o事件,执行与i/o相关的回调**(包括文件io和网络io)**。所有的事件循环以及回调处理都在这个阶段执行。其他情况nodejs将会在适当时进行阻塞
  5. check:执行setImmediate的回调函数,但不是立即执行,而是poll阶段中没有新的事件处理时再执行(即setImmediate的回调函数会在所有回调函数执行完再执
  6. close callbacks:执行一些关闭的回调函数,如 socket.on(‘close’, …)。

注意:
setTimeout 如果不设置时间或者设置时间为 0,则会默认为 1ms。此时,主流程执行完成后,超过 1ms 时,会将 setTimeout 回调函数逻辑插入到待执行回调函数poll 队列中;

2. 循环发起点

Node.js 事件循环的发起点有 4 个:

  • Node.js 启动后;
  • setTimeout 回调函数;
  • setInterval 回调函数;
  • 也可能是一次 I/O 后的回调函数。

3. 循环任务及优先级

事件循环的主要包含微任务和宏任务:

微任务

node中的微任务在事件循环中优先级最高(即优先执行),主要包括
Promise和Process.nextTick,且Process.nextTick优先级高于promise

宏任务

在 Node.js 中宏任务包含 4 种——setTimeout、setInterval、setImmediate 和
I/O。宏任务在微任务执行之后执行
,因此在同一个事件循环周期内,如果既存在微任务队列又存在宏任务队列,那么优先将微任务队列清空,再执行宏任务队列。在主线程处理回调函数的同时,同理也需要判断是否插入微任务和宏任务。

案例1:

const fs = require('fs');

setTimeout(() => { // 新的事件循环的起点

    console.log('1'); 

    fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {

        if (err) throw err;

        console.log('read file sync success');

    });

}, 0);

/// 回调将会在新的事件循环之前

fs.readFile('./config/test.conf', {encoding: 'utf-8'}, (err, data) => {

    if (err) throw err;

    console.log('read file success');

});

/// 该部分将会在首次事件循环中执行

Promise.resolve().then(()=>{

    console.log('poll callback');

});

// 首次事件循环执行

console.log('2');



在上面代码中,有 2 个宏任务和 1 个微任务,宏任务是 setTimeout 和 fs.readFile,微任务是 Promise.resolve。

整个过程优先执行主线程的第一个事件循环过程,所以先执行同步逻辑,先输出 2。

接下来执行微任务,输出 poll callback。

再执行宏任务中的 fs.readFile 和 setTimeout,由于 fs.readFile 优先级高,先执行 fs.readFile。但是处理时间长于 1ms,因此会先执行 setTimeout 的回调函数,输出 1。这个阶段在执行过程中又会产生新的宏任务 fs.readFile,因此又将该 fs.readFile 插入宏任务队列。

最后由于只剩下宏任务了 fs.readFile,因此执行该宏任务,并等待处理完成后的回调,输出 read file sync success。

结果为:

2
poll callback
1
read file success
read file sync success

注意:当同时碰到setTimeout和I/O的红任务,只有当setTimout设置时间为0时,其回调才会必定优先比I/o回调优先执行,除此之外,二者的回调执行顺序不可保证。

案例2:

const fs = require('fs');

setTimeout(() => { // 新的事件循环的起点

    console.log('1'); 

    sleep(10000)

    console.log('sleep 10s');

}, 0);

/// 将会在 poll 阶段执行

fs.readFile('./test.conf', {encoding: 'utf-8'}, (err, data) => {

    if (err) throw err;

    console.log('read file success');

});

console.log('2');

/// 函数实现,参数 n 单位 毫秒 ;

function sleep ( n ) { 

    var start = new Date().getTime() ;

    while ( true ) {

        if ( new Date().getTime() - start > n ) {

            // 使用  break  实现;

            break;

        }

    }

}

结果为:

2
1
sleep 10s
read file success

发现在setTimout的回调函数中执行sleep10秒的过程中,这里会发现 fs.readFile其实已经处理完了,并且通知回调到了主线程,但是由于主线程在处理setTimout的回调时被阻塞了10s,导致无法处理 fs.readFile的回调。因此可以得出一个结论,主线程会因为回调函数的执行而被阻塞

node不是在各个线程中为每个请求执行所有的任务,而是把任务添加到任务队列中。由一个单独的线程执行事件循环,事件循环获取事件队列中的头部任务,执行该任务,再找下一个任务。当执行到长期运行或有阻塞的代码,它不是直接调用函数,而是将函数与一个要在此函数完成后执行的回调函数一起添加到事件队列中

上述例子中如果把setTimeOut时间写大一点,导致文件IO回调比setTimou优先执行就不会出现那样的问题了

4. 循环终点

当所有的微任务和宏任务都清空的时候,虽然当前没有任务可执行了,但是也并不能代表循环结束了。因为可能存在当前因为其他回调函数阻塞导致还未回调的异步I/O,所以这个循环是没有终点的,只要进程在,并且有新的任务存在,就会去执行。

5. 单线程or多线程?

主线程js代码是单线程执行的,但是 Node.js 存在多线程执行,多线程包括 setTimeout 和异步 I/O 事件。其实 Node.js 还存在其他的线程,包括垃圾回收、内存优化等。

Node中主要是主线程来循环遍历当前事件。通过线程池来完成非阻塞IO操作

你可能感兴趣的:(Node.js,node.js,javascript)