Node简介
mindmap
Node的特点
异步I/O
事件与回调函数
单线程
跨平台
mindmap
Node的应用场景
I/O密集型
通过合理调度,适用CPU密集型
与遗留系统和平共处
分布式应用
mindmap
Node受欢迎的原因
前后端编程语言环境统一
Node带来的高性能I/O用于实时应用
并行I/O使得使用者可以更高效地利用分布式环境
并行I/O,有效利用稳定接口提升web渲染能力
云计算平台提供Node支持
游戏开发领域
工具类应用
模块机制
CommonJS规范
- 模块引用
- 模块定义
- 模块标识
Node的模块实现
引入模块流程:
graph TD
路径分析 --> 文件定位 --> 编译执行
模块分两类:
- 核心模块
在Node源代码的编译过程中,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以这部分核心模块引入时,文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,所以它的加载速度是最快的。
- 文件模块
在运行时动态加载,需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢。
Node对引入过的模块都会进行缓存,以减少二次引入时的开销。缓存的是编译和执行之后的对象。
模块调用栈
C/C++内建模块属于最底层的模块,它属于核心模块,主要提供API给JavaScript核心模块和第三方JavaScript文件模块调用。如果你不是非常了解要调用的C/C++内建模块,请尽量避免通过Process.binding()方法直接调用,这是不推荐的。
JavaScript核心模块主要扮演的职责有两类:一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用;一类是纯粹的功能模块,它不需要跟底层打交道,但是又十分重要。
异步I/O
为什么用异步I/O
- 用户体验
- 资源分配
事件循环
Node自身的执行模型
在进程启动时,Node便会创建一个类似于while(true)的循环,每执行一次循环体的过程我们称为Tick。每个Tick的过程就是查看是否有事件待处理,如果有,就取出事件及其相关的回调函数。如果存在关联的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出流程。
以下资料参考:
非I/O的异步API
- 定时器
- setTimeout
- setInterval
- process.nextTick()
每次调用process.nextTick方法, 只会将回调函数放入队列中,在下一轮Tick时取出执行。定时器中采用红黑树的操作时间复杂度为0(lg(n)),nextTick()的时间复杂度为0(1),相较之下,process.nextTick()更高效。
- setImmediate
和process.nextTick()类似,都是将回调函数延迟执行。
但process.nextTick()中的回调函数执行的优先级要高于setImmediate()
在具体实现上,process.nextTick()的回调函数保存在一个数组中,setImmediate()的结果则是保存在链表中。在行为上,process.nextTick()在每轮循环中会将数组中的回调函数全部执行完毕,而setImmediate()在每轮循环中执行链表中的一个回调函数。
异步编程
mindmap
异步编程的难点
异常处理
函数嵌套过深
阻塞代码
多线程编程
异步转同步
mindmap
异步编程解决方案
事件发布/订阅模式
Promise/Deferred模式
流程控制库
内存控制
V8的垃圾回收机制
V8的垃圾回收策略主要基于分代式垃圾回收机制。
在V8中,主要将内存分为新生代和老生代。新生代的对象为存活时间较短的对象,老生代的对象为存活时间较长或常驻内存的对象
新生代的对象主要通过Scavenge算法进行垃圾回收(空间换时间)
对象从新生代移动到老生代中的过程称为晋升。 对象晋升的条件主要有两个,一个是对象是否经历过Scavenge回收,一个是To空间的内存占用比超过限制。
老生代中主要采用了Mark-Sweep和Mark-Compact相结合的方式进行垃圾回收。
Mark-Sweep(标记清除):
在标记阶段遍历堆中所有对象,并标记活着的对象,在随后的清除阶段中,只清除没有被标记的对象。
Mark-Sweep最大的问题是在进行一次标记清除回收后,内存空间会出现不连续的状态。这种内存碎片会对后续的内存分配造成问题。
为解决Mark-Sweep的内存碎片问题,Mark-Compact被提出来。Mark-Compact是标记整理的意思。差别在于对象在标记为死亡后,在整理的过程中,将活着的对象往一端移动,移动完成后。直接清理掉边界外的内存。
在取舍上,V8主要使用Mark-Sweep,在空间不足以对从新生代晋升过来的对象进行分配时才使用Mark-Compact。
为了避免出现JavaScript应用逻辑与垃圾回收器看到的不一致的情况,垃圾回收的3种基本算法都需要将应用逻辑暂停下来,待执行完垃圾回收后再恢复执行应用逻辑,这种行为被称为 “全停顿”。
为了降低全堆垃圾回收带来的停顿时间,V8先从标记阶段入手,将原本要一口气停顿完成的动作改为增量标记,也就是拆分为许多小“步进”,没做完一“步进”,就让JavaScript应用逻辑执行一小会儿,垃圾回收与应用逻辑交替执行直接标记阶段完成。
V8后续还引入了延迟清理与增量式整理,让清理与整理动作也变成增量式的。同时还计划引入并行标记与并行清理,进一步利用多核性能降低每次停顿的时间。
触发垃圾回收的有:
- 作用域:函数作用域,with、全局作用域
- 闭包
Node中的内存并非都是通过V8进行分配,我们将哪些不是通过V8分配的内存称为堆外内存
Node的内存构成主要由通过V8进行分配的部分和Node自行分配的部分。受V8的垃圾回收限制的主要是V8的堆内存。
内存泄漏
造成内存泄漏的原因有如下几个:
- 缓存
- 队列消费不及时
- 作用域未释放
注意:
- 慎将内存当做缓存
- 关注队列状态
理解Buffer
为了高效的使用申请来的内存,Node采用了slab分配机制(动态内存管理机制)。 slab就是一块申请好的固定大小的内存区域。有如下3种状态:
- full: 完全分配状态
- partial: 部分分配状态
- empty: 没有被分配状态
Node以8KB为界限来区分Buffer是大对象还是小对象。这个8KB的值也就是每个slab的大小值。
网络编程
构建网络应用
玩转进程
服务模型的变迁
- 同步
- 复制进程:每个连接都需要一个进程来服务
- 多线程: 一个线程服务一个请求
- 事件驱动