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文件.下面是简化过的:
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
this.filename = null;
this.loaded = false;
this.children = [];
} module.exports = Module; var module = new Module(filename, parent);
上面代码中,Node定义了一个构造函数Module,所有的模块都是Module的实例.可以看到,当前模块(module.js)也是Module的一个实例.
每个实例都要自己的属性.新建一个脚本看看这些属性的值是什么.
// a.js console.log('module.id: ', module.id);
console.log('module.exports: ', module.exports);
console.log('module.parent: ', module.parent);
console.log('module.filename: ', module.filename);
console.log('module.loaded: ', module.loaded);
console.log('module.children: ', module.children);
console.log('module.paths: ', module.paths);
运行这个脚本:
$ node a.js module.id: .
module.exports: {}
module.parent: null
module.filename: /home/ruanyf/tmp/a.js
module.loaded: false
module.children: []
module.paths: [ '/home/ruanyf/tmp/node_modules',
'/home/ruanyf/node_modules',
'/home/node_modules',
'/node_modules' ]
可以看到,如果没有父模块,直接调用当前模块,parent属性就是null,id属性就是一个点.filename属性是模块的绝对路径,path属性是一个数组,包含模块肯的位置
另外,输出这些内容时,模块还没有全部加载,所以loaded属性为false.
新建另一个脚本文件b.js让其调用a.js
// b.js var a = require('./a.js'); //运行b.js $ node b.js module.id: /home/ruanyf/tmp/a.js
module.exports: {}
module.parent: { object }
module.filename: /home/ruanyf/tmp/a.js
module.loaded: false
module.children: []
module.paths: [ '/home/ruanyf/tmp/node_modules',
'/home/ruanyf/node_modules',
'/home/node_modules',
'/node_modules' ]
上面代码中,由于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的源码.
Module._load = function(request, parent, isMain) { // 计算绝对路径
var filename = Module._resolveFilename(request, parent); // 第一步:如果有缓存,取出缓存
var cachedModule = Module._cache[filename];
if (cachedModule) {
return cachedModule.exports; // 第二步:是否为内置模块
if (NativeModule.exists(filename)) {
return NativeModule.require(filename);
} // 第三步:生成模块实例,存入缓存
var module = new Module(filename, parent);
Module._cache[filename] = module; // 第四步:加载模块
try {
module.load(filename);
hadException = false;
} finally {
if (hadException) {
delete Module._cache[filename];
}
} // 第五步:输出模块的exports属性
return module.exports;
};
上面代码中,首先解析出模块的绝对路径(filename),以作为模块的识别符.然后,如果模块已经在缓存中,就从缓存取出;如果不在缓存中,就加载模块.
因此,Module._load的关键步骤是两个.
Module._resovleFilename():确定模块的绝对路径
module.load():加载模块
四.模块的绝对路径
下面是Module._resolveFilename方法的源码.
Module._resolveFilename = function(request, parent) { // 第一步:如果是内置模块,不含路径返回
if (NativeModule.exists(request)) {
return request;
} // 第二步:确定所有可能的路径
var resolvedModule = Module._resolveLookupPaths(request, parent);
var id = resolvedModule[0];
var paths = resolvedModule[1]; // 第三步:确定哪一个路径为真
var filename = Module._findPath(request, paths);
if (!filename) {
var err = new Error("Cannot find module '" + request + "'");
err.code = 'MODULE_NOT_FOUND';
throw err;
}
return filename;
};
上面代码中.在Module.resolveFilename方法内部,又调用了两个方法Module.resolveLookupPaths()和Module._findPath(),前者用来列出可能的路径,猴子用来确认
哪一个路径为真.
Module._resolveLookupPaths()的运行结果.
[ '/home/ruanyf/tmp/node_modules',
'/home/ruanyf/node_modules',
'/home/node_modules',
'/node_modules'
'/home/ruanyf/.node_modules',
'/home/ruanyf/.node_libraries',
'$Prefix/lib/node' ]
上面的模块就是模块所有可能的路径.基本是是从当前路径上一级向上寻找node_modules子目录.最后那三个路径,主要是为了历史原因保持兼容,实际上已经很少用了.
有了可能的路径以后,下面就是Module._findPath()的源码,用来确定到底哪一个是正确路径.
Module._findPath = function(request, paths) { // 列出所有可能的后缀名:.js,.json, .node
var exts = Object.keys(Module._extensions); // 如果是绝对路径,就不再搜索
if (request.charAt(0) === '/') {
paths = [''];
} // 是否有后缀的目录斜杠
var trailingSlash = (request.slice(-1) === '/'); // 第一步:如果当前路径已在缓存中,就直接返回缓存
var cacheKey = JSON.stringify({request: request, paths: paths});
if (Module._pathCache[cacheKey]) {
return Module._pathCache[cacheKey];
} // 第二步:依次遍历所有路径
for (var i = 0, PL = paths.length; i < PL; i++) {
var basePath = path.resolve(paths[i], request);
var filename; if (!trailingSlash) {
// 第三步:是否存在该模块文件
filename = tryFile(basePath); if (!filename && !trailingSlash) {
// 第四步:该模块文件加上后缀名,是否存在
filename = tryExtensions(basePath, exts);
}
} // 第五步:目录中是否存在 package.json
if (!filename) {
filename = tryPackage(basePath, exts);
} if (!filename) {
// 第六步:是否存在目录名 + index + 后缀名
filename = tryExtensions(path.resolve(basePath, 'index'), exts);
} // 第七步:将找到的文件路径存入返回缓存,然后返回
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
} // 第八步:没有找到文件,返回false
return false;
};
经过上面代码,就可以找到模块的绝对路径了.
有时在项目代码中,需要调用模块的绝对路径,那么除了module.filename,Node还提供一个require.resolve方法,供外部调用,用于从模块名渠道绝对路径.
require.resolve = function(request) {
return Module._resolveFilename(request, self);
}; // 用法
require.resolve('a.js')
// 返回 /home/ruanyf/tmp/a.js
五.加载模块
有了模块的绝对路径,就可以加载该模块了.下面是module.load方法的源码.
Module.prototype.load = function(filename) {
var extension = path.extname(filename) || '.js';
if (!Module._extensions[extension]) extension = '.js';
Module._extensions[extension](this, filename);
this.loaded = true;
};
上面代码中,首先确定模块的后缀名,不同的后缀名对应不同的加载方法.下面是.js和.json后缀名对应的处理方法.
Module._extensions['.js'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
module._compile(stripBOM(content), filename);
}; Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
这里只讨论js文件的加载.首先,将模块文件读取成字符串,然后剥离utf8编码特有的BOM文件头,最后编译该模块.
module._compile方法用于模块的编译.
Module.prototype._compile = function(content, filename) {
var self = this;
var args = [self.exports, require, self, filename, dirname];
return compiledWrapper.apply(self.exports, args);
};
上面的代码等同于下面的形式.
(function (exports, require, module, __filename, __dirname) {
// 模块源码
});
也就是说,模块的加载实质上就是注入exports,requrie,module三个全局变量,然后执行的源码,然后将模块的export编码的值输出.
原文地址:http://www.ruanyifeng.com/blog/2015/05/require.html
require用法及源码解析的更多相关文章
- airbnb 开源reAir 工具 用法及源码解析(一)
reAir 有批量复制与增量复制功能 今天我们先来看看批量复制功能 批量复制使用方式: cd reair ./gradlew shadowjar -p main -x test # 如果是本地tabl ...
- Mybatis 系列8-结合源码解析select、resultMap的用法
[Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...
- Mybatis 系列7-结合源码解析核心CRUD 配置及用法
[Mybatis 系列10-结合源码解析mybatis 执行流程] [Mybatis 系列9-强大的动态sql 语句] [Mybatis 系列8-结合源码解析select.resultMap的用法] ...
- jQuery2.x源码解析(设计篇)
jQuery2.x源码解析(构建篇) jQuery2.x源码解析(设计篇) jQuery2.x源码解析(回调篇) jQuery2.x源码解析(缓存篇) 这一篇笔者主要以设计的角度探索jQuery的源代 ...
- 【安卓网络请求开源框架Volley源码解析系列】定制自己的Request请求及Volley框架源码剖析
通过前面的学习我们已经掌握了Volley的基本用法,没看过的建议大家先去阅读我的博文[安卓网络请求开源框架Volley源码解析系列]初识Volley及其基本用法.如StringRequest用来请求一 ...
- [源码解析] PyTorch 流水线并行实现 (5)--计算依赖
[源码解析] PyTorch 流水线并行实现 (5)--计算依赖 目录 [源码解析] PyTorch 流水线并行实现 (5)--计算依赖 0x00 摘要 0x01 前文回顾 0x02 计算依赖 0x0 ...
- [源码解析] PyTorch 流水线并行实现 (6)--并行计算
[源码解析] PyTorch 流水线并行实现 (6)--并行计算 目录 [源码解析] PyTorch 流水线并行实现 (6)--并行计算 0x00 摘要 0x01 总体架构 1.1 使用 1.2 前向 ...
- [源码解析] PyTorch 分布式(11) ----- DistributedDataParallel 之 构建Reducer
[源码解析] PyTorch 分布式(11) ----- DistributedDataParallel 之 构建Reducer 目录 [源码解析] PyTorch 分布式(11) ----- Dis ...
- Nodejs源码解析之module
modulejs的导入 Require函数详解 module路径解析 module.js的导入 module.js是由node.js在Nodejs程序启动的时候导入的.module.js中使用的req ...
随机推荐
- ThinkPHP 3.2.2 事务
手册里说得非常清楚 : 5.3.19 事务支持 ThinkPHP提供了单数据库的事务支持,如果要在应用逻辑中使用事务,可以参考下面的方法: 启动事务: PHP代码 $User->startTra ...
- Oracle数据库体系结构(2)数据库实例
Oracle实例的概念: 实例(Instance):就是数据库管理系统,处于用户与物理数据库之间的一个中间层软件,由一系列内存结构和后台进程组成. 用户操作数据库的过程实质上与数据库实例建立连接,然后 ...
- Linux平台下贪吃蛇游戏的运行
1.参考资料说明: 这是一个在Linux系统下实现的简单的贪吃蛇游戏,同学找帮忙,我就直接在Red Hat中调试了一下,参考的是百度文库中"maosuhan"仁兄的文章,结合自己的 ...
- mac manpages 汉化
默认在终端进行man命令,如:man ls,会显示英文的帮助文档.本文教你如何查看中文文档. 资源:1.manpages-zh-1.5.2.tar.bz22.groff-1.21.tar.gz - ...
- php 实现简单抽奖
首先有一组数据,里面有中奖的物品和概率 $base_data = [ ['name'=>'特等奖','num'=>1], ['name'=>'一等奖','num'=>5], [ ...
- 【leetcode刷题笔记】Merge Intervals
Given a collection of intervals, merge all overlapping intervals. For example,Given [1,3],[2,6],[8,1 ...
- Docker 数据管理-Volumes
Volumes是Docker最为推荐的数据持久化方法. Volumes have several advantages over bind mounts: Volumes are easier to ...
- mysql 索引技巧
索引是快速搜索的关键.MySQL索引的建立对于MySQL的高效运行是很重要的.下面介绍几种常见的MySQL索引类型. 在数据库表中,对字段建立索引可以大大提高查询速度.假如我们创建了一个 mytabl ...
- <linux报错解决>在Fedora21下安装vmware报错的解决办法
关于VMWARE WORKSTATION在Fedora21下的安装问题 (1)在Fedora21下安装vmware如果在终端下启动,提示你找不到内核头文件Kernel Headers的话使用命令: s ...
- 算法(Algorithms)第4版 练习 1.5.3
id数组和treesize数组变化情况: 0 1 2 3 4 5 6 7 8 9 1 1 1 1 1 1 1 1 1 1 10 components 9 0 1 2 3 4 5 6 7 8 9 1 1 ...