笔记篇-深入浅出Node

138 阅读5分钟

模版机制

在Node中引入模块,需要经历如下3个步骤:

(1) 路径分析

(2) 文件定位

(3) 编译执行

模块分为两类:(1)Node提供的模块(核心模块);(2)用户编写的模块(文件模块)

  • 核心模块:
    • 在Node源码编译时就被编译为二进制文件
    • 在Node启动时被加载进内存
所以核心模块在被引用时,只需要经历路径分析阶段,加载速度最快~
  • 文件模块:
    • Node运行时动态加载
    • 需要完整的路径分析、文件定位、编译执行过程,速度比核心模块慢~

模块加载过程

(1)优先从缓存加载:Node会缓存编译和执行之后的对象,key为文件路径,value为模块,缓存在Module._cache对象上(减少二次引入时的开销)

(2)路径分析和文件定位:对于不同的标识符,模块的查找和定位有不同程度上的差异。

  1. 模块标识符分析

    require()方法接受一个标识符作为参数,标识符类型包含:

    1. 核心模块:如http、fs、path等(加载时间仅次于缓存加载)

    2. 文件模块:.或..开头的相对路径文件模块以及/开头的相对路径(先将路径转为真实路径,并以真实路径作为索引,将编译执行后的结果存放到缓存中,以使二次加载时更快,加载时间仅次于核心模块)

    3. 自定义模块:非路径形式的文件模块,如自定义的connect模块(加载时间最慢)

      自定义模块不能和核心模块标识符冲突

  2. 模块路径生成法则

    例如创建module_path.js文件,将其放到任意一个目录中然后执行node module_path.js,输出结果为一个路径数组,包含:

    1. 当前文件目录下: '/home/jackson/research/node_modules',

    2. 父目录下:'/home/jackson/node_modules',

    3. 父目录的父目录下:'/home/node_modules',

    4. 持续查找,直到根目录下:'/node_modules'

    生成法则与原型链或作用域链的查找方式十分类似。可以看出,当前文件的路径越深,模块查找耗时会越多,这是自定义模块的加载速度是最慢的原因。

  3. 文件定位

    由于缓存策略,二次引入时不需要路径分析、文件定位和编译执行的过程,大大提高了再次加载模块时的效率。

    文件定位时的细节:

    • 文件扩展名分析:会按.js、.json、.node的次序补足扩展名,依次尝试

      尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在

    • 目录分析和包:require()通过分析文件扩展名之后,可能没有查找到对应文件,但却得到一个目录,此时Node会将目录当做一个包来处理。

      在这个过程中,Node对CommonJS包规范进行了一定程度的支持:

      (1)首先,Node在当前目录下查找package.json(CommonJS包规范定义的包描述文件),通过JSON.parse()解析出包描述对象,从中取出main属性指定的文件名进行定位。如果文件名缺少扩展名,将会进入扩展名分析的步骤。

      (2)如果main属性指定的文件名错误,或者压根没有package.json文件,Node会将index当做默认文件名,然后依次查找index.js、index.json、index.node。

      (3)如果在目录分析的过程中没有定位成功任何文件,则自定义模块进入下一个模块路径进行查找。如果模块路径数组都被遍历完毕,依然没有查找到目标文件,则会抛出查找失败的异常。

  4. 模块编译

    编译和执行是引入文件模块的最后一个阶段,定位到具体的文件后,Node会新建一个模块对

    象,然后根据路径载入并编译。对于不同的文件扩展名,其载入方法也有所不同:

    • js文件:通过fs模块同步读取文件后编译执行

    • json文件:通过fs模块同步读取文件后,用JSON.parse()解析返回结果

    • node文件:通过dlopen()方法加载最后编译生成的文件

    • 其余扩展名文件:都被当做.js文件载入

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

  5. Node对CommonJS模块规范的实现

    在编译的过程中,Node对获取的JavaScript文件内容进行了头尾包装。在头部添加了(function (exports, require, module, __filename, __dirname) {\n,在尾部添加了\n}),使得每个模块文件之间都进行了作用域隔离

    一个正常的JavaScript文件会被包装成如下的样子:

(function (exports, require, module, __filename, __dirname) {

var math = require('math');

exports.area = function (radius) {

return Math.PI * radius * radius;

};

});

1、包装之后的代码会通过vm原生模块的runInThisContext()方法执行,返回一个具体的function对象。最后,将当前模块对象的exports属性、require()方法、module(模块对象自身),以及在文件定位中得到的完整文件路径和文件目录作为参数传递给这个function()执行。

2、在执行之后,模块的exports属性被返回给了调用方。exports属性上的任何方法和属性都可以被外部调用到,但是模块中的其余变量或属性则不可直接被调用。

核心模块

核心模块分为C/C++编写的和JavaScript编写的两部分,其中C/C++文件存放在Node项目的src目录下,JavaScript文件存放在lib目录下

lib目录下的js文件也需要经历包装,与文件模块有区别的地方在于:获取源代码的方式(核心模块是从内存中加载的:process.binding('natives'))以及缓存执行结果的位置(NativeModule._cache,文件模块在Module._cache

function NativeModule(id) { 
    this.filename = id + '.js'; this.id = id; 
    this.exports = {}; this.loaded = false; 
} 
NativeModule._source = process.binding('natives'); 
NativeModule._cache = {};

核心模块主要扮演的职责有两类: 一类是作为C/C++内建模块的封装层和桥接层,供文件模块调用; 一类是纯粹的功能模块