module 在 nodejs 里是一个很是核心的内容,本文经过结合 nodejs 的源码简单介绍 nodejs 中模块的加载方式和缓存机制。若是有理解错误的地方,请及时提醒纠正。javascript
ppt 地址:http://47.93.21.106/sharing/m...html
提到 nodejs 中的模块,就不能不提到 CommonJS。大部分人应该都知道 nodejs 的模块规范是基于 CommonJS 的,但其实 CommonJS 不只仅定义了关于模块的规范,完整的规范在这里:CommonJS。内容很少,感兴趣的同窗能够浏览一下。固然重点是在 模块 这一章,若是仔细读一下 CommonJS 中关于模块的规定,能够发现和 node 中的模块使用是很是吻合的。java
CommonJS 中关于模块的规定主要有三点:node
Requiregit
模块引入的方式和行为,涉及到经常使用的 `require()`。
Module Contextgithub
模块的上下文环境,涉及到 `module` 和 `exports`。
Module Identifiers面试
模块的标识,主要用于模块的引入
在 node.js 里使用模块的方式很简单,通常咱们都是这么用的:json
// format.js const moment = require('moment'); /* 格式化时间戳 */ exports.formatDate = function (timestamp) { return moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); }
上面是一个 format.js 文件,内容比较简单,引入了 moment 模块,并导出了一个格式化时间的方法供其余模块使用。bootstrap
可是你们有没有考虑过,这里的 require
和 exports
是在哪里定义的,为何咱们能够直接拿来使用呢?api
实际上,nodejs 加载文件的时候,会在文件头尾分别添加一段代码:
头部添加
(function (exports, require, module, filename, dirname) {
尾部添加
});
最后处理成了一个函数,而后才进行模块的加载:
(function (exports, require, module, __filename, __dirname) { // format.js const moment = require('moment'); /* 格式化时间戳 */ exports.formatDate = function (timestamp) { return moment(timestamp).format('YYYY-MM-DD HH:mm:ss'); } });
因此 exports
, require
, module
其实都是在调用这个函数的时候传进来的了。
这里还有两个比较细微的点,也是在不少面试题里面会出现的
经过 var
、let
、const
定义的变量变成了局部变量;没有经过关键字声明的变量会泄露到全局
exports
是一个形参,改变 exports
的引用不会起做用
第一点是做用域的问题,第二点能够问到 js 的参数传递是值传递仍是引用传递。
固然,若是只是这样讲,好像只是个人一面之词,怎么证实 nodejs 确实是这样包装的呢,这里能够用两个例子来证实:
➜ echo 'dvaduke' > bad.js ➜ node bad.js /Users/sunhengzhe/Documents/learn/node/modules/demos/bad.js:1 (function (exports, require, module, __filename, __dirname) { dvaduke ^ ReferenceError: dvaduke is not defined at Object.<anonymous> (/Users/sunhengzhe/Documents/learn/node/modules/demos/bad.js:1:63) at Module._compile (module.js:569:30) at Object.Module._extensions..js (module.js:580:10) at Module.load (module.js:503:32) at tryModuleLoad (module.js:466:12) at Function.Module._load (module.js:458:3) at Function.Module.runMain (module.js:605:10) at startup (bootstrap_node.js:158:16) at bootstrap_node.js:575:3
我在 bad.js 里面随便输入了一个单词,而后运行这个文件,能够看到运行结果会抛出异常。在异常信息里面咱们会惊讶地发现 node 把那行函数头给打印出来了,而在 bad.js 里面是只有那个单词的。
➜ echo 'console.log(arguments)' > arguments.js ➜ node arguments.js { '0': {}, '1': { [Function: require] resolve: [Function: resolve], main: Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js', loaded: false, children: [], paths: [Array] }, extensions: { '.js': [Function], '.json': [Function], '.node': [Function] }, cache: { '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js': [Object] } }, '2': Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js', loaded: false, children: [], paths: [ '/Users/sunhengzhe/Documents/learn/node/modules/demos/node_modules', '/Users/sunhengzhe/Documents/learn/node/modules/node_modules', '/Users/sunhengzhe/Documents/learn/node/node_modules', '/Users/sunhengzhe/Documents/learn/node_modules', '/Users/sunhengzhe/Documents/node_modules', '/Users/sunhengzhe/node_modules', '/Users/node_modules', '/node_modules' ] }, '3': '/Users/sunhengzhe/Documents/learn/node/modules/demos/arguments.js', '4': '/Users/sunhengzhe/Documents/learn/node/modules/demos' }
在 arguments.js 这个文件里打印出 argumens 这个参数,咱们知道 arguments 是函数的参数,那么打印结果能够很好的说明 node 往函数里传入了什么参数:第一个是 exports
,如今固然是空,第二个是 require
,是一个函数,第三个是 module
对象,还有两个分别是 __filename
和 __dirname
发现这个地方以后我相信你们都会对 nodejs 的源码感兴趣,而 nodejs 自己是开源的,咱们能够在 github 上找到 nodejs 的源码:node
实际上包装模块的代码就在 /lib/module.js
里面:
Module.prototype._compile = function(content, filename) { content = internalModule.stripShebang(content); // create wrapper function var wrapper = Module.wrap(content); var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); // ... }
_compile
函数是编译 nodejs 文件会执行的方法,函数中的 content
就是咱们文件中的内容,能够看到调用了一个 Module.wrap 方法,那么 Module.wrap 作了什么呢?这里须要找到另外一个文件,包含内置模块定义的 /lib/internal/bootstrap_node.js
,里面有对 wrap 的操做:
NativeModule.wrap = function(script) { return NativeModule.wrapper[0] + script + NativeModule.wrapper[1]; }; NativeModule.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ];
确实是前面说到的,添加函数头尾的内容。
其实知道这个处理以后,咱们能够开一些奇怪的脑洞,好比写一段好像会报错的文件:
// inject.js }); (function () { console.log('amazing');
这个文件看起来没头没尾,可是通过 nodejs 的包装后,是能够运行的,会打印出 amazing
,看起来颇有意思。
上面只是带你们看了一下 module.js 里的一小段代码,实际上若是要搞明白 nodejs 模块运做的机制,有三个文件是比较核心的:
/lib/module.js
加载非内置模块
/lib/internal/module.js
提供一些相关方法
/lib/internal/bootstrap_node
定义了加载内置模块的 NativeModule,同时这也是 node 的入口文件
咱们知道 node 的底层是 C 语言编写的,node 运行是,会调用 node.cc 这个文件,而后会调用 bootstrap_node 文件,在 bootstrap_node 中,会有一个 NativeModule 来加载 node 的内置模块,包括 module.js,而后经过 module.js 加载非内置模块,好比用户自定义的模块。(因此说模块是多么基础)
调用关系以下:
下面重点介绍一下 module。在 nodejs 里面,一般一个文件就表明了一个模块,而 module 这个对象就表明了当前这个模块。咱们能够尝试打印一下 module:
echo "console.log(module)" > print-module.js node print-module.js
打印结果以下:
Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demo-1.js', loaded: false, children: [], paths: [ '/Users/sunhengzhe/Documents/learn/node/modules/node_modules', '/Users/sunhengzhe/Documents/learn/node/node_modules', '/Users/sunhengzhe/Documents/learn/node_modules', '/Users/sunhengzhe/Documents/node_modules', '/Users/sunhengzhe/node_modules', '/Users/node_modules', '/node_modules' ] }
能够看到 module 这个对象有不少属性,exports 咱们先不说了,它就是这个模块须要导出的内容。filename 也不说了,文件的路径名。paths 很明显,是当前文件一直到根路径的全部 node_modules 路径,查找第三方模块时会用到它。咱们下面介绍一下 id、parent、children 和 loaded。
在 nodejs 里面,模块的 id 分两种状况,一种是当这个模块是入口文件时,此时模块的 id 为 .
,另外一种当模块不是入口文件时,此时模块的 id 为模块的文件路径。
举个例子,当文件是入口文件时:
➜ echo 'console.log(module.id)' > demo-1-single-file.js ➜ node demo-1-single-file.js .
此时 id 为 .
当文件不是入口文件时:
➜ cat demo-2-require-other-file.js const other = require('./demo-1-single-file'); console.log('self id:', module.id); ➜ node demo-2-require-other-file.js /Users/sunhengzhe/Documents/learn/node/modules/demos/demo-1-module-id/demo-1-single-file.js self id: .
运行 demo-2-require-other-file.js,首先打印出 demo-1-single-file 的内容,能够发现此时 demo-1-single-file 的 id 是它的文件名:由于它如今不是入口文件了。而做为入口文件的 demo-2-require-other-file.js 的 id 变成了 .
这两个含义很明确,是模块的调用方和被调用方。
若是咱们直接打印一个入口文件的 module,结果以下:
➜ echo 'console.log(module)' > demo-1-single-file.js ➜ node demo-1-single-file.js Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', loaded: false, children: [], paths: [...] }
篇幅限制,就不显示 paths 了。能够看到 parent 为 null:由于没有人调用它;children 为空:由于它没有调用别的模块。那么咱们再新建一个文件引用一下这个模块:
➜ cat demo-2-require-other-file.js require('./demo-1-single-file'); console.log(module); ➜ node demo-2-require-other-file.js Module { id: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', exports: {}, parent: Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-2-require-other-file.js', loaded: false, children: [ [Circular] ], paths: [...] }, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', loaded: false, children: [], paths: [...] } ------------------------ Module { id: '.', exports: {}, parent: null, filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-2-require-other-file.js', loaded: false, children: [ Module { id: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', exports: {}, parent: [Circular], filename: '/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-2-parent/demo-1-single-file.js', loaded: true, children: [], paths: [Array] } ], paths: [...] }
上面输出了两个 module,为了方便阅读,我用分割线分隔了一下。第一个 module 是 demo-1-single-file 打印出来的,它的 parent 如今有值了,由于 demo-2-require-other-file.js 引用它了。它的 children 依旧是空,毕竟它没有引用别人。
而 demo-2-require-other-file.js 的 parent 为 null,children 有值了,能够看到就是 demo-1-single-file。
注意里面还出现了 [Circular],由于 demo-1-single-file 的 parent 的 children 就是它本身,为了防止循环输出,nodejs 在这里省略掉了,应该很好理解。
loaded 从字面意思上也好理解,表明这个模块是否已经加载完了。但咱们会发如今上面的全部输出中,loaded 都是 false。
➜ cat demo-1-print-sync.js console.log(module.loaded); ➜ node demo-1-print-sync.js false
咱们能够在 node 的下一个 tick 里面去输出,就能获得正确的 loaded 的值了:
➜ cat demo-2-print-next-tick.js setImmediate(function () { console.log(module.loaded); }); ➜ node demo-2-print-next-tick.js true
模块究竟是如何加载的?在 /lib/module.js
里,能够找到模块加载的函数 _load
,这里 node 的注释很好地描述了加载的次序:
// Check the cache for the requested file. // 1. If a module already exists in the cache: return its exports object. // 2. If the module is native: call `NativeModule.require()` with the // filename and return the result. // 3. Otherwise, create a new module for the file and save it to the cache. // Then have it load the file contents before returning its exports // object. Module._load = function(request, parent, isMain) { //...
翻译一下,大概就是这个流程:
有缓存(二次加载)
直接读取缓存内容
无缓存(首次加载或清空缓存以后)
路径分析
文件定位
编译执行
首先看一下无缓存的状况。nodejs 首先须要对文件进行定位,找到文件才能进行加载,其实全部的细节都隐藏在了 require
方法里面,咱们调用 require,nodejs 返回模块对象,那么 require 是怎么找到咱们须要的模块的呢?
简单来说,大体是:
尝试加载核心模块
尝试以文件形式加载
X
X.js
X.json
X.node
尝试做为目录查找,寻找 package.json 文件,尝试加载 main 字段指定的文件
尝试做为目录查找,寻找 index.js、index.json、index.node
尝试做为第三方模块进行加载
抛出异常
这里涉及的代码细节比较复杂,建议先直接阅读 nodejs 的官方文档,文档对定位的顺序描述的很是详细:https://nodejs.org/dist/latest-v8.x/docs/api/modules.html#modules_all_together
若是有缓存的话,会直接返回缓存内容。好比这里有个文件,内容就是打印一行星号:
➜ cat print.js console.log('********');
若是咱们在另外一个文件里引入这个文件两次,那么会输出两行星号吗?
➜ demo-5-cache cat demo-1-just-print-multiply.js require('./print'); require('./print'); ➜ demo-5-cache node demo-1-just-print-multiply.js ********
答案是不会的,由于第一次 require 后,nodejs 会把文件缓存起来,第二次 require 直接取得缓存的内容,参考 /lib/module.js
中的代码:
Module._load = function(request, parent, isMain) { var cachedModule = Module._cache[filename]; if (cachedModule) { // 更新 parent 的 children updateChildren(parent, cachedModule, true); return cachedModule.exports; } //... Module._cache[filename] = module; tryModuleLoad(module, filename); //... }
那么,若是咱们要清空缓存,势必须要清除 Module._cache
中的内容。然而在文件里,咱们只能拿到 module 对象,拿不到 Module 类:
➜ cat demo-2-get-Module.js console.log(Module) ➜ node demo-2-get-Module.js /Users/sunhengzhe/Documents/learn/node/modules/demos/demo-5-cache/demo-2-get-Module.js:1 (function (exports, require, module, __filename, __dirname) { console.log(Module) ^ ReferenceError: Module is not defined at Object.<anonymous> (/Users/sunhengzhe/Documents/learn/node/modules/demos/demo-5-cache/demo-2-get-Module.js:1:75) at Module._compile (module.js:569:30) at Object.Module._extensions..js (module.js:580:10) at Module.load (module.js:503:32) at tryModuleLoad (module.js:466:12) at Function.Module._load (module.js:458:3) at Function.Module.runMain (module.js:605:10) at startup (bootstrap_node.js:158:16) at bootstrap_node.js:575:3
可是是否没有办法去清空缓存了呢?固然是有的。这里咱们先看 require 是怎么来的。
以前提到,require 是经过函数参数的方式传入模块的,那么咱们能够看一下,传入的 require 的究竟是什么?回到 _compile
方法:
Module.prototype._compile = function(content, filename) { content = internalModule.stripShebang(content); // create wrapper function var wrapper = Module.wrap(content); var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); // ... var require = internalModule.makeRequireFunction(this); result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); // ... return result; }
简化后的代码如上,函数内容通过包装以后生成了一个新的函数 compiledWrapper
,而后把一些参数传了进去。咱们能够看到 require 是从一个 makeRequireFunction
的函数中生成的。
而 makeRequireFunction
函数是在 /lib/internal/module.js
中定义的,看下代码:
function makeRequireFunction(mod) { const Module = mod.constructor; function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } } function resolve(request) { return Module._resolveFilename(request, mod); } require.resolve = resolve; require.main = process.mainModule; // Enable support to add extra extension types. require.extensions = Module._extensions; require.cache = Module._cache; return require; }
若是咱们直接打印 require,其实就和这里面定义的 require 是同样的:
➜ cat demo-1-require.js console.log(require.toString()); ➜ node demo-1-require.js function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } }
其实这个 require 也没有作什么事情,又调用了 mod 的 require,而 mod 是经过 makeRequireFunction
传进来的,传入的是 this,因此归根到底,require 是 module 原型上的方法,也就是 module.prototype.require,参考 /lib/module.js
中的代码。
固然这里咱们先不用追究 require 的实现方式,而是注意到 makeRequireFunction
中对 require 的定义,咱们能够发现一行关于 _cache 的代码:
function makeRequireFunction(mod) { // ... function require(path) {} require.cache = Module._cache; //.. return require; }
因此 nodejs 很贴心地,把 Module._cache
返回给咱们了,其实只要清空 require.cache 便可。而根据上面的代码,Module._cache
是经过 filename 来做为缓存的 key 的,因此咱们只须要清空模块对应的文件名。
针对上面提到的例子,清空 print.js
的缓存:
require('./print'); // delete cache delete require.cache[require.resolve('./print')]; require('./print');
而后再打印一下
➜ node demo-1-just-print-multiply.js ******** ********
就是两行星号了。
这里用到了 require 的一个 resolve 方法,它和直接调用 require 方法比较像,都能找到模块的绝对路径名,但直接 require 还会加载模块,而 require.resolve()
只会找到文件名并返回。因此这里利用文件名将 cache 里对应的内容删除了。
本文介绍了一些 nodejs 中的源码内容,在学习 nodejs 的过程当中,若是想查看 nodejs 的源码(我以为这是一个必备的过程),那么就须要去调试源码,打几个 log 看一下是否是和你预期的一致,这里说一下怎么调试 nodejs 的源码。
下载 node 源码 git@github.com:nodejs/node.git
进入源码目录执行 ./configure
& make -j
上一步以后会在 ${源码目录}/out/Release/node
里生成一个执行文件,将这个文件做为你的 node 执行文件。
每次修改源码后从新执行 make
命令。
好比修改代码以后,运行 make
,而后这样运行文件便可:
➜ /Users/sunhengzhe/Documents/project/source-code/node/out/Release/node demo-1-single-file.js