require用法及源码解析

一.require()的基本用法

  require语句内部逻辑:

当 Node 遇到 require(X) 时,按下面的顺序处理。

(1)如果 X 是内置模块(比如 require('http')) 
  a. 返回该模块。 
  b. 不再继续执行。

(2)如果 X 以 "./" 或者 "/" 或者 "../" 开头 
  a. 根据 X 所在的父模块,确定 X 的绝对路径。 
  b. 将 X 当成文件,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

  • X
  • X.js
  • X.json
  • X.node

  c. 将 X 当成目录,依次查找下面文件,只要其中有一个存在,就返回该文件,不再继续执行。

  • X/package.json(main字段)
  • X/index.js
  • X/index.json
  • X/index.node

(3)如果 X 不带路径 
  a. 根据 X 所在的父模块,确定 X 可能的安装目录。 
  b. 依次在每个目录中,将 X 当成文件名或目录名加载。

(4) 抛出 "not found"

请看一个例子。

当前脚本文件 /home/ry/projects/foo.js 执行了 require('bar') ,这属于上面的第三种情况。Node 内部运行过程如下。

首先,确定 x 的绝对路径可能是下面这些位置,依次搜索每一个目录。


/home/ry/projects/node_modules/bar
/home/ry/node_modules/bar
/home/node_modules/bar
/node_modules/bar

搜索时,Node 先将 bar 当成文件名,依次尝试加载下面这些文件,只要有一个成功就返回。


bar
bar.js
bar.json
bar.node

如果都不成功,说明 bar 可能是目录名,于是依次尝试加载下面这些文件。


bar/package.json(main字段)
bar/index.js
bar/index.json
bar/index.node

如果在所有目录中,都无法找到 bar 对应的文件或目录,就抛出一个错误。

 

 二.Module构造函数

  源码:

  require的源码早Node的lib/module.js文件.下面是简化过的:

  

  
 1 function Module(id, parent) {
 2   this.id = id;
 3   this.exports = {};
 4   this.parent = parent;
 5   this.filename = null;
 6   this.loaded = false;
 7   this.children = [];
 8 }
 9 
10 module.exports = Module;
11 
12 var module = new Module(filename, parent);
View Code

   上面代码中,Node定义了一个构造函数Module,所有的模块都是Module的实例.可以看到,当前模块(module.js)也是Module的一个实例.

   每个实例都要自己的属性.新建一个脚本看看这些属性的值是什么.

   

1 // a.js
2 
3 console.log('module.id: ', module.id);
4 console.log('module.exports: ', module.exports);
5 console.log('module.parent: ', module.parent);
6 console.log('module.filename: ', module.filename);
7 console.log('module.loaded: ', module.loaded);
8 console.log('module.children: ', module.children);
9 console.log('module.paths: ', module.paths);
View Code

 

  运行这个脚本:

  

 1 $ node a.js
 2 
 3 module.id:  .
 4 module.exports:  {}
 5 module.parent:  null
 6 module.filename:  /home/ruanyf/tmp/a.js
 7 module.loaded:  false
 8 module.children:  []
 9 module.paths:  [ '/home/ruanyf/tmp/node_modules',
10   '/home/ruanyf/node_modules',
11   '/home/node_modules',
12   '/node_modules' ]
View Code

 

    可以看到,如果没有父模块,直接调用当前模块,parent属性就是null,id属性就是一个点.filename属性是模块的绝对路径,path属性是一个数组,包含模块肯的位置

    另外,输出这些内容时,模块还没有全部加载,所以loaded属性为false.

  新建另一个脚本文件b.js让其调用a.js

  

 1 // b.js
 2 
 3 var a = require('./a.js');
 4 
 5 //运行b.js
 6 
 7 $ node b.js
 8 
 9 module.id:  /home/ruanyf/tmp/a.js
10 module.exports:  {}
11 module.parent:  { object }
12 module.filename:  /home/ruanyf/tmp/a.js
13 module.loaded:  false
14 module.children:  []
15 module.paths:  [ '/home/ruanyf/tmp/node_modules',
16   '/home/ruanyf/node_modules',
17   '/home/node_modules',
18   '/node_modules' ]
View Code

     上面代码中,由于a.js被b.js调用,所以parent属性指向b.js模块,id属性和filename属性一直,都是模块的绝对路径.

三.模块实例的require方法

  每个模块实例都有一个require方法.

    Module.prototype.require =funtion (path){

      return Module._load(path,this);

    };

  由此可知,require并不是全局命令,而是每个模块提供的一个内部方法,也就是说,只有在模块内部才能使用requrie命令(额日益例外是REPL环境).

  另外,requrie其实内部调用Module._load方法.

  下面来看Module._load的源码.

  

 1 Module._load = function(request, parent, isMain) {
 2 
 3   //  计算绝对路径
 4   var filename = Module._resolveFilename(request, parent);
 5 
 6   //  第一步:如果有缓存,取出缓存
 7   var cachedModule = Module._cache[filename];
 8   if (cachedModule) {
 9     return cachedModule.exports;
10 
11   // 第二步:是否为内置模块
12   if (NativeModule.exists(filename)) {
13     return NativeModule.require(filename);
14   }
15 
16   // 第三步:生成模块实例,存入缓存
17   var module = new Module(filename, parent);
18   Module._cache[filename] = module;
19 
20   // 第四步:加载模块
21   try {
22     module.load(filename);
23     hadException = false;
24   } finally {
25     if (hadException) {
26       delete Module._cache[filename];
27     }
28   }
29 
30   // 第五步:输出模块的exports属性
31   return module.exports;
32 };
View Code

  

  上面代码中,首先解析出模块的绝对路径(filename),以作为模块的识别符.然后,如果模块已经在缓存中,就从缓存取出;如果不在缓存中,就加载模块.

  因此,Module._load的关键步骤是两个.

    Module._resovleFilename():确定模块的绝对路径

    module.load():加载模块

四.模块的绝对路径

  下面是Module._resolveFilename方法的源码.

  

 1 Module._resolveFilename = function(request, parent) {
 2 
 3   // 第一步:如果是内置模块,不含路径返回
 4   if (NativeModule.exists(request)) {
 5     return request;
 6   }
 7 
 8   // 第二步:确定所有可能的路径
 9   var resolvedModule = Module._resolveLookupPaths(request, parent);
10   var id = resolvedModule[0];
11   var paths = resolvedModule[1];
12 
13   // 第三步:确定哪一个路径为真
14   var filename = Module._findPath(request, paths);
15   if (!filename) {
16     var err = new Error("Cannot find module '" + request + "'");
17     err.code = 'MODULE_NOT_FOUND';
18     throw err;
19   }
20   return filename;
21 };
View Code

 

上面代码中.在Module.resolveFilename方法内部,又调用了两个方法Module.resolveLookupPaths()和Module._findPath(),前者用来列出可能的路径,猴子用来确认

哪一个路径为真.

Module._resolveLookupPaths()的运行结果.

1 [   '/home/ruanyf/tmp/node_modules',
2     '/home/ruanyf/node_modules',
3     '/home/node_modules',
4     '/node_modules' 
5     '/home/ruanyf/.node_modules',
6     '/home/ruanyf/.node_libraries'7      '$Prefix/lib/node' ]
View Code

 

上面的模块就是模块所有可能的路径.基本是是从当前路径上一级向上寻找node_modules子目录.最后那三个路径,主要是为了历史原因保持兼容,实际上已经很少用了.

有了可能的路径以后,下面就是Module._findPath()的源码,用来确定到底哪一个是正确路径.

 

 1 Module._findPath = function(request, paths) {
 2 
 3   // 列出所有可能的后缀名:.js,.json, .node
 4   var exts = Object.keys(Module._extensions);
 5 
 6   // 如果是绝对路径,就不再搜索
 7   if (request.charAt(0) === '/') {
 8     paths = [''];
 9   }
10 
11   // 是否有后缀的目录斜杠
12   var trailingSlash = (request.slice(-1) === '/');
13 
14   // 第一步:如果当前路径已在缓存中,就直接返回缓存
15   var cacheKey = JSON.stringify({request: request, paths: paths});
16   if (Module._pathCache[cacheKey]) {
17     return Module._pathCache[cacheKey];
18   }
19 
20   // 第二步:依次遍历所有路径
21   for (var i = 0, PL = paths.length; i < PL; i++) {
22     var basePath = path.resolve(paths[i], request);
23     var filename;
24 
25     if (!trailingSlash) {
26       // 第三步:是否存在该模块文件
27       filename = tryFile(basePath);
28 
29       if (!filename && !trailingSlash) {
30         // 第四步:该模块文件加上后缀名,是否存在
31         filename = tryExtensions(basePath, exts);
32       }
33     }
34 
35     // 第五步:目录中是否存在 package.json 
36     if (!filename) {
37       filename = tryPackage(basePath, exts);
38     }
39 
40     if (!filename) {
41       // 第六步:是否存在目录名 + index + 后缀名 
42       filename = tryExtensions(path.resolve(basePath, 'index'), exts);
43     }
44 
45     // 第七步:将找到的文件路径存入返回缓存,然后返回
46     if (filename) {
47       Module._pathCache[cacheKey] = filename;
48       return filename;
49     }
50   }
51 
52   // 第八步:没有找到文件,返回false 
53   return false;
54 };
View Code

 

经过上面代码,就可以找到模块的绝对路径了.

有时在项目代码中,需要调用模块的绝对路径,那么除了module.filename,Node还提供一个require.resolve方法,供外部调用,用于从模块名渠道绝对路径.

1 require.resolve = function(request) {
2   return Module._resolveFilename(request, self);
3 };
4 
5 // 用法
6 require.resolve('a.js')
7 // 返回 /home/ruanyf/tmp/a.js
View Code

 五.加载模块

  有了模块的绝对路径,就可以加载该模块了.下面是module.load方法的源码.

  

1 Module.prototype.load = function(filename) {
2   var extension = path.extname(filename) || '.js';
3   if (!Module._extensions[extension]) extension = '.js';
4   Module._extensions[extension](this, filename);
5   this.loaded = true;
6 };
View Code

   上面代码中,首先确定模块的后缀名,不同的后缀名对应不同的加载方法.下面是.js和.json后缀名对应的处理方法.

  

 1 Module._extensions['.js'] = function(module, filename) {
 2   var content = fs.readFileSync(filename, 'utf8');
 3   module._compile(stripBOM(content), filename);
 4 };
 5 
 6 Module._extensions['.json'] = function(module, filename) {
 7   var content = fs.readFileSync(filename, 'utf8');
 8   try {
 9     module.exports = JSON.parse(stripBOM(content));
10   } catch (err) {
11     err.message = filename + ': ' + err.message;
12     throw err;
13   }
14 };
View Code

  这里只讨论js文件的加载.首先,将模块文件读取成字符串,然后剥离utf8编码特有的BOM文件头,最后编译该模块.

  module._compile方法用于模块的编译.

  

1 Module.prototype._compile = function(content, filename) {
2   var self = this;
3   var args = [self.exports, require, self, filename, dirname];
4   return compiledWrapper.apply(self.exports, args);
5 };
View Code

  上面的代码等同于下面的形式.

  

1 (function (exports, require, module, __filename, __dirname) {
2   // 模块源码
3 });
View Code

  也就是说,模块的加载实质上就是注入exports,requrie,module三个全局变量,然后执行的源码,然后将模块的export编码的值输出.

  原文地址:http://www.ruanyifeng.com/blog/2015/05/require.html

posted @ 2016-03-18 13:39  我想我是海~~  阅读(1376)  评论(0编辑  收藏  举报