Node的模块实现

  1. node中引入模块,需要经历3个步骤
  2. 1. 路径分析
  3. 2. 文件定位
  4. 3. 编译执行
  1. node模块分为两类
  2. 1. Node提供的模块,核心模块
  3. 2. 用户编写的模块,文件模块

核心模块部分在node源代码编译时,编译进了二进制执行文件。在Node进程启动时,部分核心模块就被直接加载进内存中,所以文件定位和编译执行这两个步骤可以省略掉,并且在路径分析中优先判断,加载速度最快。

文件模块是在运行时动态加载,需要完整的路径分析、文件定位、编译执行,速度较慢。

1. 模块的加载过程

require()方法会对相同模块的二次加载一律采用缓存优先的方式,这是第一优先级的。核心模块缓存检查先于文件模块的缓存检查。

2. 路径分析和文件定位

模块标识符分析

  • 核心模块
    http, fs, path,优先级仅次于缓存加载,如果试图加载一个与核心模块标识符相同的自定义模块,那是无法成功的。如果自己编写了一个http模块,必须换一个不同的标识符或采用路径方式。
  • 路径形式
    ., .., /开始的标识符,分析时会先将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果缓存。加载速度慢于核心模块。
  • 自定义模块
    这类模块查找最费时。因为它会去查找模块路径。模块路径是Node在定位文件模块的具体文件时制定的查找策略,具体表现为一个路径组成的数组。
  1. //1. 创建module_path.js文件,内容为
  2. console.log(module.paths);
  3. //2. 放到任意目录下,执行 node module_path.js
  4. //3. 得到这样一个数组:
  5. [
  6. '/Users/nali/Desktop/nardo.li/keeplearning/node_study/node_modules',
  7. '/Users/nali/Desktop/nardo.li/keeplearning/node_modules',
  8. '/Users/nali/Desktop/nardo.li/node_modules',
  9. '/Users/nali/Desktop/node_modules',
  10. '/Users/nali/node_modules',
  11. '/Users/node_modules',
  12. '/node_modules'
  13. ]
  14. //可以看出来模块路径的生成规则与js的原型链或作用域链查找方式相似
  15. //从当前目录下递归向父级目录查找目标文件。所以文件路径越深越耗时

文件定位

当不包含文件扩展名时,Node会按.js,.json,.node次序补足扩展名。由于尝试时,node会调用fs模块同步阻塞式地判断文件是否则存在,所以会引起性能问题。解决方案:

  1. 如果是.node和.json文件,传递时带上扩展名;
  2. 同步配合缓存,大幅度缓解node单线程中阻塞式调用的缺陷。

3. 模块编译

在Node中,每个文件模块都是一个对象,它的定义如下:

  1. function Module(id, parent){
  2. this.id = id;
  3. this.exports = {};
  4. this.parent = parent;
  5. if(parent && parent.children){
  6. parent.children.push(this);
  7. }
  8. this.filename = null;
  9. this.loaded = false;
  10. this.children = [];
  11. }

定位到具体文件后,Node会新建一个模块对象,然后根据路径载入并编译,对于不同的文件扩展名,其载入方法也有所不同:

  • .js文件 通过fs模块同步读取文件后编译执行。
  • .node文件 这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件
  • .json文件 通过fs模块同步读取文件后,用JSON.parse()解析返回结果。
  • 其余 都被当做.js文件载入

每一个编译成功的模块都会将其文件路径作为索引缓存在Module._cache对象上,以提高二次引入的性能。
**.json**文件的调用

  1. //Native extension for .json
  2. Module._extensions['.json'] = function(module, filename){
  3. var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
  4. try{
  5. module.exports = JSON.parse(stripBOM(content));
  6. }catch(err){
  7. err.message = filename + ': ' + err.message;
  8. throw err;
  9. }
  10. }

Module._extensions会被require()的extensions属性,所以通过在代码中访问require.extensions可以知道系统中已有的扩展加载方式。

  1. //编写如下代码测试:
  2. console.log(require.extensions);
  3. //控制台得到的结果如下:
  4. { '.js': [Function], '.json': [Function], '.node': [Function] }

javascript模块的编译

在编译的过程中,Node对获取的javascript文件内容进行了头尾包装。在头部添加了(function(exports, require, module, __filename, __dirname){});

  1. //一个正常的js文件会被包装成如下:
  2. (function(exports, require, module, __filename, __dirname){
  3. var math = require('math');
  4. exports.area = function(radius){
  5. return Math.PI * radius * radius;
  6. };
  7. })
  8. //这样每个模块文件之间都进行了作用域隔离。

tips

兼容多种模块规范

  1. //以下代码演示如何将hello()方法定义到不同的运行环境中
  2. //它能够兼容Node、AMD、CMD以及常见的浏览器环境中
  3. (function(name, definition){
  4. //检测上下文环境是否为AMD或者CMD
  5. var hasDefine = typeof define === 'function',
  6. hasExports = typeof module !== 'undefined' && module.exports;
  7. if(hasDefine){
  8. // AMD环境或CMD环境
  9. define(definition);
  10. }else if(hasExports){
  11. //定义为普通Node模块
  12. module.exports = definition();
  13. }else {
  14. //将模块的执行结果挂在window变量中,在浏览器中this指向window对象
  15. this[name] = definition();
  16. }
  17. })('hello', function(){
  18. var hello = function(){};
  19. return hello;
  20. })