Node.js是一个构建在Chrome浏览器V8引擎
上的JavaScript运行环境, 使用单线程
、事件驱动
、非阻塞I/O
的方式实现了高并发请求,libuv
为其提供了异步编程的能力。javascript
从这张图上咱们能够看出,Node.js底层框架由Node.js标准库、Node bindings、 底层库三个部分组成。java
这一层是由Javascript编写的,也就是咱们使用过程当中直接能调用的API,在源码中的lib目录下能够看到,诸如http、fs、events
等经常使用核心模块node
这一层能够理解为是javascript与C/C++库之间创建链接的桥
, 经过这个桥,底层实现的C/C++库暴露给javascript环境,同时把js传入V8
, 解析后交给libuv
发起非阻塞I/O
, 并等待事件循环
调度;linux
这一层主要有如下四块:编程
顺带看一下libuv的架构图,可见Nodejs的网络I/O
、文件I/O
、DNS操做
、还有一些用户代码都是在libuv工做的。promise
咱们知道任务调度通常有两种方案: 一是单线程串行执行
,执行顺序与编码顺序一致,最大的问题是没法充分利用多核CPU,当并行极大的时候,单核CPU理论上计算能力是100%; 另外一种就是多线程并行处理
,优势是能够有效利用多核CPU,缺点是建立与切换线程开销大,还涉及到锁、状态同步等问题, CPU常常会等待I/O结束,CPU的性能就白白消耗。浏览器
一般为客户端链接建立一个线程须要消耗2M内存,因此理论上一台8G的服务器,在Java应用中最多支持的并发数是4000。而Node.js只使用一个线程,当有客户端链接请求时,触发内部事件,经过非阻塞I/O,事件驱动机制,让其看起来是并行的。 理论上一台8G内存的服务器,能够同时容纳3到4万用户的链接。bash
Node.js采用单线程方案,免去锁、状态同步等繁杂问题,又能提升CPU利用率。Node.js高效的除了由于其单线程外,还必须配合下面要说的非阻塞I/O。服务器
首先要清楚,对于一个网络IO,会涉及到两个系统对象:网络
而当一个读操做发生时,它会经历两个阶段:
接下来理清这几个概念:
select/poll
这个function会不断的轮询所负责的全部socket,当某个socket有数据到达了,就通知用户进程。 而epool
经过callback回调通知机制.减小内存开销,不因并发量大而下降效率,linux下最高效率的I/O事件机制。阻塞I/O,非阻塞I/O,多路复用I/O
都属于同步I/O。 注意非阻塞I/O在数据从内核拷贝到用户进程时,进程仍然是阻塞的,因此仍是属于同步I/O。总结:
阻塞I/O和非阻塞I/O区别在于:在I/O操做的完成或数据的返回前是等待仍是返回(能够理解成一直等仍是分时间段等) 同步I/O和异步I/O区别在于 :在I/O操做的完成或数据的返回前会不会将进程阻塞(或者说是主动查询仍是被动等待通知)
因为Node.js中采用了非阻塞型I/O机制,所以在执行了读数据的代码以后,将当即转而执行其后面的代码,把读数据返回结果的处理代码放在回调函数中,从而提升了程序的执行效率。 当某个I/O执行完毕时,将以事件的形式通知执行I/O操做的线程,线程执行这个事件的回调函数。
执行栈
(execution context stack);事件队列
(Event queue),当用户的网络请求或者其它的异步操做
到来时,会先进入到事件队列中排队,并不会当即执行它,代码也不会被阻塞,继续往下走,直到主线程代码执行完毕;事件循环机制
(Event Loop),检查队列中是否有要处理的事件,从队头取出第一个事件,从线程池
分配一个线程来处理这个事件,而后是第二个,第三个,直到队列中全部事件都执行完了。 当有事件执行完毕后,会通知主线程,主线程执行回调,并将线程归还给线程池。这个过程就叫事件循环
(Event Loop);注意:
FIFO(先进先出)执行回调函数的队列
,一般当事件循环进入到给定阶段会执行特定于该阶段的全部操做,而后执行该阶段队列的回调事件直到队列耗尽或者超过最大执行限度为止,而后事件循环就会走向下一阶段;阶段概述:
setTimeout
和setInterval
调度的回调。I/O回调函数
。I/O事件
回调,在适当的条件下 node 会阻塞在这个阶段。setImmediate
的回调。事件循环的 Pending
、Idle/Prepare
和 Close
阶段涂成灰色,由于这些是 Node 在内部使用的阶段。
Node.js开发者编写的代码仅以微任务形式在主线
、计时器(Timers)
阶段、轮询(Poll)
阶段和 查询(Check)
阶段中运行。
总有一种维持 poll 状态的倾向
;后续 tick 各个阶段是否存在不为空的回调函数队列
和 最近的计时器时间节点
决定。 若全部队列为空且不存在任何计时器,那么事件循环将 无限制地维持在 poll 阶段
;以实现一旦存在 I/O 回调函数加入到 poll 队列中便可当即获得执行;poll 阶段主要有两个功能:
process.nextTick() 不在 Event Loop 的任何阶段执行,而是在各个阶段切换的中间执行
,即从一个阶段切换到下个阶段前执行。
这里还须要提一下 macrotask 和 microtask 的概念,macrotask(宏任务)指 Event Loop 每一个阶段执行的任务,microtask(微任务)指每一个阶段之间执行的任务。
即上述 6 个阶段都属于 macrotask,process.nextTick() 属于 microtask
。
process.nextTick() 的实现和 v8 的 microtask 并没有关系,是 Node.js 层面的东西,应该说 process.nextTick() 的行为接近为 microtask。 Promise.then 也属于 microtask 的一种。
能够经过递归 process.nextTick()调用来“饿死”您的 I/O,阻止事件循环到达 轮询 阶段。
promise.then 回调像微处理同样执行,就像 process.nextTick 同样。 虽然,若是二者都在同一个微任务队列中,则将首先执行 process.nextTick 的回调
。 优先级 process.nextTick > promise.then = queueMicrotask
咱们来看这一段代码在不一样的环境下执行的结果:
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
复制代码
首先在浏览器环境下,其输出结果为:
timer1
promise1
timer2
promise2
复制代码
借助于前面关于浏览器中Javascript事件循环的知识,不能理解。
而后咱们将其在 Node.js v11.0.0
如下版本执行,获得结果:
timer1;
timer2;
promise1;
promise2;
复制代码
但若是是在 Node.js v11.0.0
以上(包括)版本中执行中,将获得结果:
timer1;
promise1;
timer2;
promise2;
复制代码
缘由是 node v11 如下只有所有执行了 timers 阶段队列的所有任务才执行微任务队列,而浏览器只要执行了一个宏任务就会执行微任务队列。 node v11 在 timer 阶段的 setTimeout,setInterval 和在 check 阶段的 immediate 都在 node v11 里面都修改成一旦执行一个阶段里的一个任务就马上执行微任务队列。 也是为了和浏览器保持一致。
setImmediate
设计为在当前轮询 poll 阶段完成后执行脚本setTimeout
计划在以毫秒为单位的最小阈值过去以后运行脚本不在 I/O 回调(即主模块)
内的脚本,则两个计时器的执行顺序是不肯定的,由于它受机器性能的约束,好比:
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
复制代码
输出顺序是不肯定的。
咱们知道 setTimeout 的回调函数在 timer 阶段执行,setImmediate 的回调函数在 check 阶段执行,Event loop 的开始会先检查 timer 阶段,可是在开始以前到 timer 阶段会消耗必定时间,因此就会出现两种状况:
而若是这两个调用在一个I/O回调中,那么immediate老是先执行。
const fs = require('fs');
fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout');
}, 0);
setImmediate(() => {
console.log('immediate');
});
});
复制代码
分析以下:
setInterval(() => {
console.log('setInterval')
}, 100)
process.nextTick(function tick () {
process.nextTick(tick)
})
复制代码
运行结果:setInterval 永远不会打印出来。
process.nextTick 会无限循环,将 Event loop 阻塞在 microtask 阶段,致使 Event loop 上其余 macrotask 阶段的回调函数没有机会执行。 解决方法一般是用 setImmediate 替代 process.nextTick,以下:
setInterval(() => {
console.log('setInterval')
}, 100)
setImmediate(function immediate () {
setImmediate(immediate)
})
复制代码
运行结果:每 100ms 打印一次 setInterval。
process.nextTick 内执行 process.nextTick 仍然将 tick 函数注册到当前 microtask 的尾部,因此致使 microtask 永远执行不完; setImmediate 内执行 setImmediate 会将 immediate 函数注册到下一次 Event loop 的 check 阶段,而不是当前正在执行的 check 阶段,因此给了 Event loop 上其余 macrotask 执行的机会。
setImmediate(() => {
console.log('setImmediate1')
setImmediate(() => {
console.log('setImmediate2')
})
process.nextTick(() => {
console.log('nextTick')
})
})
setImmediate(() => {
console.log('setImmediate3')
})
复制代码
运行结果在 node v11如下是:
setImmediate1
setImmediate3
nextTick
setImmediate2
复制代码
在node v11以上是:
setImmediate1
nextTick
setImmediate3
setImmediate2
复制代码
缘由同案例一
setImmediate(() => {
console.log(1)
setTimeout(() => {
console.log(2)
}, 100)
setImmediate(() => {
console.log(3)
})
process.nextTick(() => {
console.log(4)
})
})
process.nextTick(() => {
console.log(5)
setTimeout(() => {
console.log(6)
}, 100)
setImmediate(() => {
console.log(7)
})
process.nextTick(() => {
console.log(8)
})
})
console.log(9)
复制代码
运行结果在 node v11如下是:
9
5
8
1
7
4
3
6
2
复制代码
在 node v11以上是:
9
5
8
1
4
7
3
6
2
复制代码
缘由请自行分析。