Node 本身能够做到如此高性能的根本原因在于事件(event)的使用,以及对于事件监听者(listener,callback)的调用上。
events 观察者包
可注册多个监听器
const EventEmitter = require('events')
const emitter = new EventEmitter()
// 注册事件
emitter.on('myEvent',function myListener(message){
console.log(message) // 1
})
// 注册一次性事件
emitter.once('myEvent',function myListener2(param1,param2){
console.log(param1, param2) // 1 2
})
// 获得所有事件
console.log(emitter.listeners('myEvent')) // myListener myListener2
// 发射事件
emitter.emit('myEvent',1,2,3,4,5,6)
// events内置事件,注册事件会触发newListener事件监听
emitter.once('newListener',(event,listener)=>{
if(event == 'myEvent'){
emitter.on('myEvent')
}
})
Node 本身是基于事件循机制的
本质上,当Node启动一个文件或者服务器后,Node实际上是运行再一个死循环中的。
white(true){ .... }
如果代码执行过程中,不会抛出异常或者异常抛出去能被处理,这一直在这死循环中运行。
在这个死循环中,当Node会不断发射事件,监听事件并且执行回调逻辑。
事件来源主要有两种:一是Node自身发射出的事件,二是来自于Node自身运行的环境。 监听事件:回调都是要依附于相应的事件的。 执行回调逻辑:本质上都是由底层来执行。
关于IO操作的异步执行逻辑
1、同步模式:
2、异步模式:poll (文件多性能降低) epoll(node大量使用)
node在linux中采用epoll调用模式,如果有事件执行,会去调用相应回调,如果没有事件执行会进入休眠,等待事件 node在windows中采用IOCP调用模式
Node单线程:所谓单线程,指的是Node的逻辑执行主线程是单线程,即javascript代码运行所处的现场,这个是单线程,因为javascript本身只能执行在单线程中。
底部执行异步调用时,是有个线程池,IOCP本身有线程池,linux通过libuv封装的。
Node对于文件操作都是异步操作,这是广义文件,普通文件是文件,socket,http,网络都是文件等
当我们在程序中引入某个第三方模块时,那么整体执行逻辑如下:
Node -> 第三方模块 -> 原生模块 -> 原生模块内部实现(internal)-> c++模块 -> libuv/iocp -> 线程池调用 -> 选择可用线程执行底层IO操作(涉及操作系统调用)
当Node在执行过程中,它会判断当前操作系统类型,libuv/iocp都不属于nodejs只是调用操作系统封装,是操作系统自己的实现
Node完整的事件逻辑
1、启动Node运行时 2、检查是否有待处理的事件 3、如果没有回到循环开始 4、如果有,则从事件队列中取出一个事件 5、判断当前这个事件有没有与之关联的事件处理器(回调) 6、如果没有,则回到循环开始 7、如果有,则执行事件的回调逻辑 8、回到循环开始,开始新一轮事件检测流程
每个Node的执行过程实际上是由完整的事件循环机制 + 底层操作系统异步IO调用 + 线程池(底层库实现由操作系统提供)共同配置完成。
对于单线程的Node来说,是否无法利用多核的优势呢?
对于Node事件循环主线程是只能运行在一个核心上面。 对于底层的线程池来说,他们却可以运行在多个核心上面,当然也可以同时运行,因此他们是可以利用到多核的优势。