因为标识符有几种形式,对于不同的标识符,模块的查找和定位有不同程度上的差异。
## 1.模块标识符分析
前面提到过,require()方法接受一个标识符作为参数。在Node实现中,正是基于这样一个标识符进行模块查找的。模块标识符在Node中主要分为以下几类:
* 核心模块,如http、fs、path等。
* . 或 .. 开始的相对路径文件模块。
* 以 / 开始的绝对路径文件模块。
* 非路径形式的文件模块,如自定义的connect模块。
### 核心模块
核心模块的优先级仅次于缓存加载,它在Node的源代码编译过程中已编译为二进制代码,其加载过程最快。
如果试图加载一个与核心模块标识符相同的自定义模块,那是不会成功的。如果自己编写了一个 http 用户模块,要想加载成功,必须选择一个不同的标识符或者换用路径的方式。
### 路径形式的文件模块
以 . 、..和 / 开始的标识符,这里都被当作文件模块来处理。在分析路径模块时,require() 方法会将路径转换为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快。
由于文件模块给Node指明了确切的文件位置,所以在查找过程中可以节约大量时间,其加载速度慢于核心模块。
### 自定义模块
自定义模块指的是非核心模块,也不是路径形式的标识符。它是一种特殊的文件模块,可能是一个文件或者包的形式。这类模块的查找是最费时的,也是所有方式中最慢的一种。
在介绍自定义模块的查找方式之前,需要先介绍一下模块路径这个概念。
模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。关于这个路径的生成规则,我们可以手动尝试一番。
(1). 创建 `module_path.js` 文件,其内容为 `console.log(module.paths);` 。
(2). 将其放入任意一个目录中然后执行 node module_path.js 。
在Linux下,你可能得到这样一个数组输出:
~~~
[ '/home/jackson/research/node_modules',
'/home/jackson/node_modules',
'/home/node_modules',
'/node_modules' ]
~~~
而在Windows下,也许是这样:
~~~
[ 'C:\\Users\\Simon\\Desktop\\NodeDemo\\node_modules',
'C:\\Users\\Simon\\Desktop\\node_modules',
'C:\\Users\\Simon\\node_modules',
'C:\\Users\\node_modules',
'C:\\node_modules' ]
~~~
可以看出,模块路径的生成规则如下:
* 当前文件目录下的node_modules目录
* 父目录下的node_modules目录
* 父目录的父目录下的node_modules目录
* 沿路径向上逐级递归,直到根目录下的node_modules目录
它的生成方式与JavaScript的原型链或作用域链的查找方式十分类似。在加载的过程中,Node会逐个尝试模块路径中的路径,直到找到目标文件为止。可以看出,当前文件的路径越深,模块查找耗时越多,这是自定义模块的加载速度最缓慢的原因。
## 2.文件定位
从缓存加载的优化策略使得二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。
但在文件定位的过程中,还有一些细节需要注意,这主要包括文件扩展名的分析、目录和包的处理。
### 文件扩展名分析
require()在分析标识符的过程中,会出现标识符中不包含文件扩展名的情况。CommonJS模块规范也允许在标识符中不包含文件扩展名,这种情况下,Node会按.js、.json、.node的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用fs模块同步阻塞式的判断文件是否存在。因为Node是单线程的,所以这里是一个会引起性能问题的地方。小诀窍是:如果是.node或者.json文件,在传递给require()方法的标识符中带上扩展名,会加快一点速度。另一个诀窍是:同步配合缓存,可以大幅度缓解Node单线程中阻塞式调用的缺陷。
### 目录分析和包
在分析标识符的过程中,require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,这在引入自定义模块和逐个模块路径进行查找时会经常出现,此时Node会将目录当作一个包来处理。
在这个过程中,Node对CommonJS包规范进行了一定程度的支持。首先,Node在当前目录下查找 package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当作默认文件名,然后依次查找index.js、index.json、index.node 。
如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有找到任何目标文件,则会抛出查找失败的异常。
- 目录
- 第1章 Node 简介
- 1.1 Node 的诞生历程
- 1.2 Node 的命名与起源
- 1.2.1 为什么是 JavaScript
- 1.2.2 为什么叫 Node
- 1.3 Node给JavaScript带来的意义
- 1.4 Node 的特点
- 1.4.1 异步 I/O
- 1.4.2 事件与回调函数
- 1.4.3 单线程
- 1.4.4 跨平台
- 1.5 Node 的应用场景
- 1.5.1 I/O 密集型
- 1.5.2 是否不擅长CPU密集型业务
- 1.5.3 与遗留系统和平共处
- 1.5.4 分布式应用
- 1.6 Node 的使用者
- 1.7 参考资源
- 第2章 模块机制
- 2.1 CommonJS 规范
- 2.1.1 CommonJS 的出发点
- 2.1.2 CommonJS 的模块规范
- 2.2 Node 的模块实现
- 2.2.1 优先从缓存加载
- 2.2.2 路径分析和文件定位
- 2.2.3 模块编译
- 2.3 核心模块
- 2.3.1 JavaScript核心模块的编译过程
- 2.3.2 C/C++核心模块的编译过程
- 2.3.3 核心模块的引入流程
- 2.3.4 编写核心模块
- 2.4 C/C++扩展模块
- 2.4.1 前提条件
- 2.4.2 C/C++扩展模块的编写
- 2.4.3 C/C++扩展模块的编译
- 2.4.2 C/C++扩展模块的加载
- 2.5 模块调用栈
- 2.6 包与NPM
- 2.6.1 包结构
- 2.6.2 包描述文件与NPM
- 2.6.3 NPM常用功能
- 2.6.4 局域NPM
- 2.6.5 NPM潜在问题
- 2.7 前后端共用模块
- 2.7.1 模块的侧重点
- 2.7.2 AMD规范
- 2.7.3 CMD规范
- 2.7.4 兼容多种模块规范
- 2.8 总结
- 2.9 参考资源
- 第3章 异步I/O
- 3.1 为什么要异步I/O
- 3.1.1 用户体验
- 3.1.2 资源分配
- 3.2 异步I/O实现现状
- 3.2.1 异步I/O与非阻塞I/O
- 3.2.2 理想的非阻塞异步I/O
- 3.2.3 现实的异步I/O
- 3.3 Node的异步I/O
- 3.3.1 事件循环
- 3.3.2 观察者
- 3.3.3 请求对象
- 3.3.4 执行回调
- 3.3.5 小结
- 3.4 非I/O的异步API
- 3.4.1 定时器
- 3.5 事件驱动与高性能服务器