
深入浅出 Node.js 读书笔记
Chapter 1 Node简介
Node 的特点:
- 异步 I/O
- 事件与回调函数机制
- 单线程
Node 采用 child_process 的方式来解决单线程中大计算量的问题:将计算量分发到各个子进程,将大量的计算分解,然后再通过进程之间的事件消息来传递结果,从而保持应用模型的简单和低依赖。
- 跨平台
主要的应用场景:
- 擅长 I/O 密集型应用场景
- CPU 密集型应用偏弱,但性能仍不俗,也可以通过编写 C/C++ 扩展的方式加强。
- 与遗留系统和平共处
- 分布式应用(阿里的 NodeFox, ITier)
Chapter 2 模块机制
Node 普遍采用 CommonJS 模块规范。
CommonJS 模块规范
模块引用:
var math = require('math')模块定义
使用exports对象作为导出当前模块的方法或者变量的唯一出口。
在模块中,还存在一个module对象,它代表模块自身,而exports是module的属性。// 模块定义 // math.js exports.add = function () { ... } // 调用方法 // program.js var math = require('math') exports.increment = function (val) { return math.add(val, 1) }
exports 和 module.exports 的区别
exports是对module的exports属性的引用。- 在使用
exports.add = function ()和module.exports.add = function()的方式输出模块时,二者没有区别; - 但如果使用
exports = add ()的情况,exports这个引用指向的地址就发生了改变。 - 但你可以使用
module.exports = add()的方法输出模块,因为require()读取的就是module.exports对象。它还可以输出除了对象和函数外的基本类型,如 string, number 等。 - 建议使用第二点的输出方式。
- 模块标识
模块标识其实就是传递给require()方法的参数。它必须是符合小驼峰命名的字符串,或者以.或..开头的相对路径,或绝对路径。它可以没有任何文件名后缀如.js。
Node 的模块实现
在 Node 中引入模块需要经历三个步骤:(1)模块标识符分析(2)文件定位(3)编译执行。
模块加载过程
- 优先从缓存加载
Node 会优先检查缓存,以减少二次引入时的开销。核心模块的缓存检查优先于文件模块的缓存检查,但缓存检查总是第一优先级的。 - 模块标识符分析
(1) 核心模块:优先级仅次于缓存加载,在 Node 源代码编译过程中已经编译为二进制代码,加载过程最快。
(2) 路径形式的文件模块:分析文件模块时,require()方法会将路径转为真实路径,并以真实路径作为索引,将编译后的结果放到缓存中。加载速度慢于核心模块。
(3) 自定义模块: - 文件定位
(1) 扩展名分析:CommonJS 规范允许标识符中不包含文件扩展名,在这种情况下 Node 会按照 .js, .json, .node 的顺序补足扩展名,依次尝试。这个过程需要调用fs模块同步阻塞地判断文件是否存在,可能引起性能问题。建议:如果是 .json 或 .node 文件,建议加上扩展名,同时配合缓存。
(2) 目录分析和包: - 模块编译
Node调用process.dlopen()方法进行加载和执行;在Node架构下,dlopen()方法在Windows和*nix平台下分别有不同的实现,通过libuv兼容层进行了封装。
核心模块
两部分:
- C/C++编写的模块,存放在Node项目的src目录下
- Javascript 编写的模块,存放在lib目录下
本章余下的 C/C++ 模块编写,模块调用栈,包与NPM,前后端共用模块均略过不提。
Chapter 3 异步I/O
为什么要异步I/O?
- 用户体验:消除掉UI阻塞的现象,继续响应用户的交互行为,给用户一个鲜活的页面。
- 资源分配:利用单线程,原理多线程死锁、状态同步等问题;利用异步I/O,让单线程远离阻塞,以更好地使用CPU。
现实的异步I/O
通过让部分线程进行阻塞I/O或者非阻塞I/O加轮询技术来完成数据获取,让一个线程进行计算处理,通过线程之间的通信将IO得到的数据进行传递,这就轻松实现了异步I/O(尽管它是模拟的)。