node.js入门之event

202 阅读5分钟

node入门小白,下面是自己的总结,如哪里有不对的地方,感谢指出来。

一、重点

1.事件循环

2.事件驱动程序   

3.EventEmitter

二、内容

1.node事件循环(看之前需先自行了解下 线程、进程)

这里先说下node.js运行机制,node.js采用v8作为js的解析引擎,而在I/O处理方面采用了libuv。libuv库负责node api的执行,它将不同的任务分给不同的线程,形成一个Event Loop。以异步的方式将执行的结果返回给V8引擎。下面直接上图:

了解了node运行机制后,咱们再来看libuv中的事件循环,其中 libuv 引擎中的事件循环分为 6 个阶段,它们会按照顺序反复运行。每当进入某一个阶段的时候,都会从对应的回调队列中取出函数去执行。当队列为空或者执行的回调函数数量到达系统设定的阈值,就会进入下一阶段(为了避免函数在poll阶段阻塞)。下面看下对应的六个阶段的图:

我理解的事件循环规则:

1.每个阶段都有会执行各自对应的回调函数

2.每个阶段在执行完各自的回到函数后,都要去检查该阶段是否有process.nextTick任务和microtask(微任务)如果有的话并去执行。

下面我们详细说下每个阶段的原理和顺序:

第一步node初始化

  • 初始化 node 环境。
  • 执行输入代码。
  • 执行 process.nextTick 回调。(后续每个阶段都会执行该操作,比作a)
  • 执行 microtasks。(后续每个阶段都会执行该操作,比作b)

第二步进入event-loop(事件循环)

(1)进入 poll 阶段

  • 首先检查是否存在尚未完成的回调,如果存在,那么分两种情况。
    • 第一种情况:
      • 如果有可用回调(可用回调包含到期的定时器还有一些IO事件等),执行所有可用回调。
      • 执行a,b,如果有,全部执行。
      • 退出该阶段。
    • 第二种情况:
      • 如果没有可用回调,检查是否有 setImmediate 回调,如果有,退出 poll 阶段。如果没有,会先等待回调,如果超时会被加入到队列中并立即执行回调,这里同样会有个超时时间设置防止一直等待下去。
  • 如果不存在尚未完成的回调,退出poll阶段。

(2) 进入check 阶段

  • 如果有setImmediate()回调,则执行所有setImmediate回调。
  • 执行a,b,如果有,全部执行。
  • 退出该阶段。

这里说下两个注意点: (1) setTimeout 和 setImmediate 二者非常相似,区别主要在于调用时机不同。setTimeout可能执行在前也可能在后,为什么这么说,因为进入事件循环也是需要成本的,如果在准备时候花费了大于 1ms 的时间,那么在 timer 阶段就会直接执行 setTimeout 回调。但当二者在异步 i/o callback 内部调用时,总是先执行 setImmediate,再执行 setTimeout 结果如下:

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout
  • setImmediate 设计在 poll 阶段完成时执行,即 check 阶段;
  • setTimeout 设计在 poll 阶段为空闲时,且设定时间到达后执行,但它在 timer 阶段执行

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

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

(3)进入 close callbacks阶段

  • 当一个socket连接或者一个handle被突然关闭时(例如调用了socket.destroy()方法),close事件会被发送到这个阶段执行回调。
  • 执行a,b,如果有,全部执行。
  • 退出该阶段。

(4)进入 timers 阶段

  • 检查 timer 队列是否有到期的 timer 回调,如果有,将到期的 timer 回调按照 timerId 升序执行。
  • 执行a,b,如果有,全部执行。
  • 退出该阶段。

**(5) **进入IOcallbacks阶段

  • 检查是否有 pending 的 I/O 回调。如果有,执行回调。如果没有,退出该阶段。
  • 执行a,b,如果有,全部执行。
  • 退出该阶段。

(6) 进入idle,prepare 阶段(这两个阶段与我们编程关系不大,暂且不说)

以上是总结的事件循环的过程和原理,整个过程我们可以换个角度去理解:

固执的探险家(每个房间都要走到底)

所有的代码就像是已经设定好的迷宫,而引擎就是去探险的人,我们称为小呆。

小呆到达迷宫,已经有了地图,根据地图冒险。

迷宫有六个房间,分别是timer, i/ocallback, ide prepare(内部使用,已封闭), poll, check, close callback,

其中timer是虚拟现实房间,小呆随时可以看到里面的场景。

其他的每个房间又五个房间,有的开放,有的不开放。

探险规则:每次离开一个房间,都要检查有没有受伤(peocess.nextTick())和检查有没有丢下物品没有处理(microtasks微任务)

小呆首先进入poll房间,开始探险(执行poll 队列),之后进入check房间,timer房间(随机),探险完之后出来,进入close callback,探险完之后,进入io/callback房间,最后完成探险,离开。

小呆说任务总算完成了。

node中的事件循环和浏览器中的事件循环有所差异不要混为一谈,浏览器中的事件循环可以先参考这里看下juejin.cn/post/684490…

本周继续更新2.事件驱动程序3.EventEmitter