美文网首页
1、node的模块实现

1、node的模块实现

作者: 萘小蒽 | 来源:发表于2019-07-15 21:23 被阅读0次

node的模块实现

Node在实现中并非完全按照规范实现,而是对模块规范进行了一定的取舍,同是也增加了少许自身需要的特性。

node在实现exports、requiremodule的过程中究竟经历了什么呢?

  • 在node中引入模块,需要经历三个步骤。
    1 . 路径分析
    2 . 文件定位
    3 . 编译执行

  • 模块又分为两类:

核心模块 Node自己提供的模块。

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

文件模块 用户编写的模块,

文件模块在运行时动态加载,需要经历完整上面的三个加载步骤,路径分析、文件定位、执行编译过程。
文件模块,引入它,三步骤不能少,加载查找缓存都在核心模块后。

详细的加载过程

  • 优先从缓存加载

和浏览器一样,缓存静态脚本文件来提高性能,node对引入过的模块都会进行缓存,以减少二次引入的开销。
不同在于浏览器缓存的仅仅是文件,而node缓存的是编译和执行之后的对象。

不管是核心模块还是文件模块,require方法对想用模块的二次加载都一律采用缓存优先的方式,这是第一优先级的,而且核心模块的缓存检查优先于文件模块。

  • 路径分析和文件定位
  1. 模块标识符分析

node中的require是基于标识符进行模块查找的。标识符无非以下四种形式。

  • 核心模块,如 http、fs、path
  • .、`..``开始的相对路径的文件模块
  • /开始的绝对路径的文件模块
  • 非路径形式夺得文件模块,如自定义的connect模块。

模块的路径是node在定位文件模式的具体文件时指定的查找策略,具体表现为一个路径组成的数组,关于策略的生成规则,我们动手尝试一下:
(1)创建module_path.js文件

//module_path.js
console.log(module.paths)

(2)将它放入任意一个目录中,然后执行module_path.js文件(windows下)

//终端代码
d:\nodeDemo> node module_path
[ 'd:\\nodeDemo\\node_modules', 'd:\\node_modules' ]

上面可以看出,模块路径的生成规则是由当前文件目录沿路径向上逐渐递归,直到根目录下的node_modules目录。
听起来有点像JavaScript的原型链以及作用域的查找方式十分类似,当文件的路径越深,查找耗时就越多,这就是自定义模块的加载速度是最慢的原因。

  1. 模块标识符分析
    从缓存中加载的优化策略使得二次引入时省略路径分析、文件定位和变异执行的过程。
    在文件的定位过程中,还有一些文件拓展名的分析、目录和包的处理细节,需要注意。

require()在分析标识符的过程中,可能会有标识符没有文件拓展名的情况,这种情况下node会按.js,.json,.node的次序补足扩展名,依次尝试。
在尝试的过程中,需要调用fs模块同步阻塞式地判断文件是否存在。因为node是单线程的,所以这里是一个回应器性能问题的地方。

  1. 目录分析和包

如果在分析文件拓展名之后,没有找到对应文件,但却得到一个目录,此时Node会将目录当做一个包来处理。

在这个过程中,首先node在当前目录下查找package.json(包的描述文件),解析出包描述对象,从中取出main属性指定的文件名进行定位。如果没有main属性指定的文件名, 或者压根没有描述文件(package.json),Node会将index当做默认文件名,依次查找index.js、index.json、index.node

  • 模块编译
    node中每个文件模块都是一个对象,定义如下:
function module(id, parent){
  this.id = id;
  this.exports = {};
  this.parent = parent;
  if(parent&&parent.children){
    parent.children.push(this)
   }
   this.filename = null;
   this.loaded = false;
   this.children = [];
}

编译和执行是引入文件模块的最后一个阶段。定位到具体的文件后,node会新建一个模块对象,然后根据路径载入并编译。

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

根据不同的文件拓展名,Node会调用不同的读取方式,如.json文件的调用如下:

//Native extension for .json
Module._extensions['.json'] = function (module, filename) {
  var content = NativeModule.require('fs').readFileSync(filename, 'utf8');
  try {
    module.exports = JSON.parse(stripBOM(content));
  } catch (err) {
    err.message = filename + " : " + err.message;
    throw err;
  }
};

这里的Module._extensions会被赋值给require()extensions属性:

console.log(Module._extensions)
// { '.js': [Function], '.json': [Function], '.node': [Function] }

1 . JavaScript模块的编译
每个模块中存在着(require、exports、module)这三个变量,甚至模块中还有_filename、_dirname ,但是模块文件中是没有定义的。

事实上,在编译过程中,Node对获取的js文件进行了头尾包装,如下:

(function(exports, require, module, _filename, _dirname){
  var math = require('math');
   exports.area = function(radius){
     return Math.pi * radius* radius;   
   };
});

每个模块文件之间进行了作用域隔离,包装之后的代码会通过vm原生模块的runInThisContext()方法执行(类似eval,但具有上下文,不污染全局),返回一个具体的function对象。最后,将当前模块对象的exports属性, require方法, module(模块对象自身), 文件定位中得到的完整文件路径(_filename)和文件目录(_dirname)作为参数传递给 这个function对象执行。

很多人不明白exportsmodule.exports两者的差异,以及module.exports存在的意义。

exports = function(){
  // my class
}

上面这种做法通常会失败,原因是exports对象是通过形参的方式传入的,直接复制会改变它的引用,但并不能改变作用域外的值。像如下:

fuinction change(a){
   a = 100;
  console.log(a); //100
};
var a = 10
change(a) //10

如果要达到require引入一个类的效果,请赋值给module.exports对象,它不会改变形参的引用。

2 . c/c++模块的编译
node调用process.dlopen()方法进行加载和执行。在Node的架构下,dlopen()方法在windows和*nix平台下分别有不同的实现,通过libuv兼容层进行了封装。

实际上,.node的模块文件并不需要编译,只需要加载和执行的过程,执行的过程中,模块的exports对象与。node模块产生联系,然后返回给调用者。

c/c++模块给node使用者带来的优势主要是执行效率方面的,但是它的门槛比JavaScript高。

3 . JSON文件的编译
.json文件的编译时三种编译方式中最简单的。Node利用fs模块同步读取JSON文件的内容之后,调用JSON.parse()方法得到对象,然后复制给模块对象的exports,以供外部调用。

相关文章

网友评论

      本文标题:1、node的模块实现

      本文链接:https://www.haomeiwen.com/subject/qinkkctx.html