阅读 805

《深入浅出Nodejs》总结、记录(一)

最近这段时间一直在看《深入浅出Nodejs》这本书,写这边文章的目的是记录那些我认为比较有用的node知识点。

node简介

node是一个基于事件驱动来实现非阻塞I/O的服务器。 node结构和chrome十分相似,它们都是基于事件驱动的异步架构,浏览器通过事件驱动来进行页面交互、Node通过事件驱动来服务I/O。

node应用场景

因为node是异步I/O,所以处理I/O密集型的任务是毋庸置疑的(I/O密集型任务包括文件操作、网络通信等),node利用事件循环的处理能力,而不是启动每一个线程为每一个请求服务,资源占用极少。而cpu密集型的计算任务主要得益于V8引擎的深度优化。

node为什么会是单线程?

单线程最大的好处是不用像多线程编程那样处处在意状态同步问题,这里没有死锁的存在,也没有线程上下文切换所带来的性能开销。

单线程所带来的缺点?

  • 无法利用多核cpu
  • 错误会引起整个应用退出,应用的健壮性值得考验
  • 大量计算占用CPU导致无法继续调用异步I/O

像浏览器端js与UI公用一个线程一样,node端cpu长时间占用也会导致后续的异步I/O无法调用,已完成的异步I/O函数也会得不到及时执行。
后来node出现了子进程child_process,遇到大量计算性的任务,可以将计算分解到各个子进程,然后通过进程间的通信来传递结果。

异步I/O

异步I/O与非阻塞I/O

异步与非阻塞听起来像是一回事,就实际效果而言,异步与非阻塞都能打到并行的目的。但是从计算机内核而言,同步/异步和阻塞/非阻塞实际上是两回事。
操作系统对于I/O只有两种方式,阻塞和非阻塞。 阻塞I/O造成CPU等待I/O,浪费时间等待,CPU的处理能力得不到充分利用。 非阻塞I/O返回之后,CPU可以处理其他事务,但是由于此时的I/O并没有完成,获取的数据并不是我们希望的数据,为了获取完整的数据,应用程序需要重复调用I/O来确认是否完成,这种重复调用判断的技术叫做轮询。

node中的异步I/O——事件循环

在node进程启动时,会有一个类似于while(true)的循环,每执行一次循环体的过程我们称之为tick。每个tick的过程就是检查是否有事件需要处理,如果有,就取出事件及其相关的回调函数。如果存在相关的回调函数,就执行它们。然后进入下个循环,如果不再有事件处理,就退出进程。

node中的异步I/O——观察者

在每个tick的过程中,每个事件循环有一个或多个观察者,而判断是否有事件要处理就的过程就是向这些观察者询问。在Node中,事件主要来自于网络请求、文件操作等,这些事件对应的观察者有网络观察者、文件I/O观察者等,观察者将事件进行了分类。

nodejs中的异步I/O是如何实现的?(与浏览器中的异步有什么不同

nodejs中的异步I/O有I/O线程池的参与。node启动时,同时会有一个事件循环启动,事件循环中有一个I/O观察者,观察是否有I/O调用结束。node中发起一个异步I/O操作时,会先封装一个请求对象,将I/O操作的参数和回调函数封装进这个对象中,然后将这个对象传递给I/O线程池,I/O线程池收到对象,会先判断线程是否可用,若是可用,执行对象中的I/O操作,将执行完的结果也放入请求对象中,然后通知I/O观察者调用完成,此时线程池回归可用状态。I/O观察者能拿到请求对象,取出请求对象中的回到函数和执行结果,执行回调函数。此时一个异步I/O操作完成。

非I/O的异步API

在node中还存在与I/O无关的API,它们分别是setTimeout、setInterval、setImmediate、process.nextTick以及setImmediate
setTimeoutsetInterval与浏览器中一致,但与异步I/O相比较就是没有I/O线程池的参与。每次setTimeoutsetInterval创建的定时器会被插入到定时器观察者内部的一个红黑树中,每次Tick执行时,会从该红黑树中取出定时器对象,检查是否超过定时时间,如果超过,就形成了一个事件,回调函数立即执行。
每次调用process.nextTick()方法,只会将回调函数放入队列中去,在下一轮Tick时取出执行。与setTimeoutsetInterval相比,process.nextTick()性能更高效,因为定时器需要动用红黑树、创建定时器对象和迭代等操作。
setImmediateprocess.nextTick()十分类似,都是讲回调函数延迟执行,但是也有一些细微的差别。process.nextTick()中回调函数优先级要高于setImmediate。原因在于事件循环对于观察者的检查是有先后顺序的。process.nextTick()属于idle观察者,setImmediate属于check观察者。在每一轮循环检查中,idle观察者要先于I/O观察者,I/O观察者先于check观察者。process.nextTick()的回调函数保存在一个数组中,setImmediate的回调函数保存在链表中。process.nextTick()每轮循环会把数组中的回调函数全部执行完,setImmediate每轮循环中执行链表中的一个回调函数。

模块机制Commonjs

模块定义

上下文中提供·require方法来引入外部模块,提供exports对象用来导出方法或者变量,并且是唯一到出口。在模块中,还有一个module对象,代表模块自身,exports就是module的一个属性。一个文件就是一个模块。
node中模块分为两类,一类是node提供的模块,称为核心模块。另一类是用户编写的模块,称为文件模块。

模块定义的意义

将类聚的方法或变量等限定在私有的作用域中,同时支持引入和导出功能以顺畅的连接上下游依赖。Commonjs这套导出出入机制使得用户完全不必考虑变量污染

引入模块经历的步骤

  1. 路径分析
  2. 文件定位
  3. 编译执行
  • 核心模块在node源代码的编译过程中,编译进了二进制执行文件。在node进程启动时,核心模块就被直接加载进内存,所以文件定位和编译执行这两步直接省略,并且路径分析中优先判断,所以加载速度最快。
  • 文件模块在运行时动态加载,需要走完成的三个流程,所以加载速度要慢一些。

浏览器会缓存静态文件脚本以提高性能、而node会缓存编译后的对象以提高性能。
不论是核心模块还是文件模块,require()引入时对相同模块都是先从缓存中加载,这是第一优先级的,只不过核心模块的缓存检查先于文件模块。
从缓存中加载的优化策略可以使二次引入时不需要路径分析、文件定位和编译执行,大大提高效率。

模块编译

  1. .js文件 通过fs模块同步读取文件然后编译执行
  2. .node文件 这时通过c/c++编写的扩展文件,通过dlopen()加载然后编译生成的文件
  3. .json文件 通过fs模块同步读取文件后,用JSON.parse解析返回的结果。

javascript模块的编译

在编译js的过程中,Node对获取的js文件内容进行头尾包装。也就是将原本的js代码用一个函数包装了起来。

(function(exports,require,module,__filename,__dirname){
    var Math = require('math');
    exports.area = function(radius){
        return Math.PI*radius
    }
});
复制代码

内存控制

垃圾回收机制

V8对象分为新生代对象(存活时间比较短)和老生代对象(存活时间比较长)。
新生代对象采用复制移动的方式实现垃圾回收,新生代内存的空间分为两个部分,一个是From空间,另一个是to空间。进行对象分配时,先在From空间进行分配,当进行垃圾回收时,会将存活对象复制到To空间,然后将From所有对象清空,在将To空间的对象复制到From空间,最后清空To空间。
在进行新生代内存垃圾回收时,会检查对象是否已经经历过一次垃圾回收,若是,会将这个对象升级为老生代对象。同时,也会检查To空间内存使用是否超过25%,若是,会将剩余的对象设置为老生代对象。
老生代对象采用标记清除法,在标记阶段遍历所有堆中的对象,并标记活着的对象,在随后的清楚阶段,只清除没有被标记的对象。
标记清除法有一个问题就是进行过一轮垃圾回收后,内存空间会是不连续的状态,这种内存碎片会对后续内存分配造成问题,所有出现了标记整理法,在回收阶段将活着的对象往一端移动,移动完成直接清理掉边界内存。

文章分类
前端
文章标签