[TOC]
Node 基础
1. Node概述
- Node API,javascript实用程序
- Node Core,用于实现Node API的一组javascript模块,基于libuv、V8引擎等
- javacript Engine,将js语言翻译成机器码,node使用V8引擎翻译所有的js代码,但是node不一定使用V8作为js引擎,也可使用其它js引擎.
- Event Loop,事件循环,Node使用libuv实现事件循环,无论是主线阶段还是事件循环阶段,都是在一个线程中完成的,实现事件循环需要使用到node异步API,将回调函数作为参数,在进行事件循环时会运行这些回调函数.
node不仅适合I/O密集型,同时CPU密集型工作也可以胜任.libuv 使用称为“工人池”的线程池,即用于卸载 I/O 密集型任务和 CPU 密集型任务的线程池。
2. Node 事件循环
1.1 非阻塞I/O
node 使用三种技术来防止阻塞业务逻辑的执行,分别是事件、异步API和非阻塞I/O。
非阻塞I/O
指程序在做其它事情的时候,该程序可以发起一个请求获取网络资源,等到网络资源获取成功后,就执行一个回调函数来处理该网络资源;
1.2 Node的事件循环
源自于IBM的一篇关于NODE的教程。
首先,粗略的解释一下什么是事件循环
(或者事件轮询
),这里很容易联想到组成原理课上关于I/O访问的那一章,最原始的I/O访问方式即程序轮循,即由处理器每隔一定时间对各个I/O端口进行访问,检查各端口有无准备就绪。事件轮询
浅显的理解为单向运行先入先出队列。
- Node生命周期
在js主线完成以后,node线程将开始执行事件循环,即执行调用node非阻塞api中的回调函数.若代码中没有非阻塞的代码,运行完主线之后,node将结束;若存在非阻塞代码,则node将在所有回调函数执行完毕后结束.
一些概念
- 任务队列(Task Queue)
上图中的每个阶段都会有一个FIFO的任务队列,一般来说,特定于某个阶段的逻辑会在开始时执行,直到队列为空或者达到系统的限制.需要值得注意的是Task Queue
并不是一个真正的Queue
,而是一个set
,它是选取第一个可执行的Task
执行,而非严格意义上的出队入队 js的异步任务分为hong任务和微任务
- 微任务(Microtask)
- 微任务(Microtask)
- 任务队列(Task Queue)
在主线和事件每个阶段完成后,会立刻执行微任务回调,Node 开发者编写的代码仅以微任务形式在主线、计时器(Timers) 阶段、轮询(Poll) 阶段和 查询(Check) 阶段中运行。普遍的观点是认为promise回调属于微任务,但在某些浏览器中(Microsoft Edge, Firefox 40, iOS Safari and desktop Safari 8.0.8 )中,会把promise的回调视作一个新的宏任务而不是微任务。
主要有:Promise.then、MutaionObserver、process.nextTick(Node.js环境)
- 宏任务(Task)
主要有:script(全局任务)、setTimeout、setInterval、I/O、UI交互事件、postMessage、requestAnimationFrame、MessageChannel、setImmediate(Node.js环境)
主要阶段说明
大体上而言,事件循环包含多个调用回调函数阶段- 计时器阶段:将运行
setInterval()
和setTimeout()
过期计时器回调函数 - 轮询阶段:将轮询操作系统以查看是否完成所有I/O操作,如果完成,运行相应的回调函数
- 检查阶段:将运行
setImmediate()
回调函数
- 计时器阶段:将运行
细节阶段说明
而按照上图,node时间循环中,更为详细的事件阶段为:Timers阶段
任何过期的计时器回调都会在事件循环的这个阶段中运行,计时器分为两类setImmediate()
Immediate计时器是一个node对象,它在下一个check阶段立即运行setTimeout()
,Timeout计时器也是一个node对象,它在计时器过期后尽快运行回调,在该计时器过期后,会在事件循环的下一个Timers阶段运行回调。Timeout计时器有两种,分别是:Interval
,该计时器由setInterval()
创建,每次该计时器过期时,就运行一次回调,只要node进程仍在运行,就会重复此过程,除非你调用了clearInterval()
Timeout
该计时器由setTimeout()
创建,超过设置的delay(默认1ms)事件后,会运行一次回调,除非在回调运行之前调用了clearTimeout()
当不再有过期的计时器回调运行时候,事件循环就会运行所有的微任务,运行微任务完成后,事件循环进入到Pending阶段
- Pending阶段
一些系统上的回调发生在本阶段执行
- Idle和Prepare阶段
- Poll阶段
此阶段执行I/O回调,如果轮询队列为空(无I/O之外的任何事件),则会阻塞并等待任何正在执行的I/O操作完成,然后立即执行这些操作的回调。如果调度了计时器,则Poll阶段将会结束,在必要时运行微任务,然后事件循环进入到Check阶段
- Check阶段
只有`setImmediate()`回调会在该阶段中执行,使用户可以在Poll阶段变得空闲时立刻执行一些代码.Check阶段的回调队列为空后,会运行所有的微任务,然后事件循环进入到Close阶段
- Close阶段
如果某个套接字(socket)或句柄突(handle)然关闭(例如,如果调用了一个套接字的 `socket.destroy()` 方法),则会执行此阶段,这种情况下会触发其close事件。
- 通过代码加深理解
const fs=require('fs');
const { setImmediate } = require('timers');
var cnt=0;
const event=()=>{
if(cnt>=1) {
clearInterval(myInterval)
}
console.log(`${cnt}: interval begin`)
fs.readFile('../.images/node-structure.png',(err,file)=>{
console.log(`${cnt}:poll begin`)
if(err){console.log(err)}
else{
console.log(file.length)
}
})
setImmediate(()=>{console.log(`${cnt}: immediate`)})
cnt++;
}
const myInterval=setInterval(event,1);
运行结果
0: interval begin
1: immediate
1: interval begin
2: immediate
2:poll begin
190428
2:poll begin
190428
其中interval以1ms进行回调,在第一次循环中,首先执行Timers阶段,此时cnt=1
,interval过期,执行回调函数,首先输出interval begin
,接着进入pending、idle、prepare阶段,接着开始poll进行I/O访问,此时I/O未准备好,到达check阶段,执行immediat,而后开始第二次循环,此时cnt=1
,首先执行clearInterval
函数,再cnt++
,Timers阶段后,cnt=2
,第二次循环后I/O仍然没有准备好,于是执行immediate函数,第二次循环完成后,回调队列只有有I/O的回调未完成,于是在第三次循环的poll阶段阻塞,直到I/O事件完成
参考
- IBM node.js之旅
- 《Node.js实战(第二版)》
- JS中的Event Loop和Task queue
- html living standard