• 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

  • 相关阅读:
    python学习笔记:遍历目录
    c++笔记:友元函数
    VMware Workstation 9: This virtual machine's policies are too old to be run by this version of VMware
    inet_ntoa内存问题
    python学习笔记:sqlite3查询
    python学习笔记:利用asyncore的端口映射(端口转发)
    编写谷歌浏览器的油猴脚本
    window编译7z
    通过配置nginx的header路由到不同环境的服务器
    用U盘给物理机安装ubuntu20.04
  • 原文地址:https://www.cnblogs.com/ZSG-DoBestMe/p/5291731.html
Copyright © 2020-2023  润新知