1.在 Node.js 模块系统中,每个文件都被视为独立的模块。
在执行模块代码之前,Node.js 将使用如下所示的函数封装器对其进行封装:
(function(exports, require, module, __filename, __dirname) {
// 模块代码实际存在于此处
});
通过这样做,Node.js 实现了以下几点:
- 它将文件内定义的顶层变量(用
var
、const
或let
定义)保持在模块而不是全局对象的范围内。 - 它有助于提供一些实际特定于模块的全局变量,例如:
module
和exports
对象,实现者可以用来从模块中导出值。- 便利变量
__filename
和__dirname
,包含模块的绝对文件名和目录路径。
2.模块在第一次加载后被缓存。
这意味着(类似其他缓存)每次调用 require(module)
都会返回完全相同的对象(如果解析为相同的文件)。
如果 require.cache
没有被修改,则多次调用 require('foo')
不会导致模块代码被多次执行。 这是重要的特征。 有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。
要让模块多次执行代码,则导出函数,然后调用该函数。
模块根据其解析的文件名进行缓存。 由于模块可能会根据调用模块的位置(从 node_modules
文件夹加载)解析为不同的文件名,因此如果 require('foo')
解析为不同的文件,则不能保证 require('foo')
将始终返回完全相同的对象。
此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的模块,并将多次重新加载文件。 例如,require('./foo')
和 require('./FOO')
返回两个不同的对象,而不管 ./foo
和 ./FOO
是否是同一个文件。
3.当文件直接从 Node.js 运行时,则 require.main
设置为其 module
。 这意味着可以通过测试 require.main === module
来确定文件是被直接运行。
4.当有循环 require()
调用时,模块在返回时可能尚未完成执行。
考虑这种情况:
a.js
:
console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');
b.js
:
console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');
main.js
:
console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);
当 main.js
加载 a.js
时,a.js
依次加载 b.js
。 此时,b.js
尝试加载 a.js
。 为了防止无限循环,将 a.js
导出对象的未完成副本返回给 b.js
模块。 然后 b.js
完成加载,并将其 exports
对象提供给 a.js
模块。
到 main.js
加载这两个模块时,它们都已完成。 因此,该程序的输出将是:
$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true
需要仔细规划以允许循环模块依赖项在应用程序中正常工作。
前端优质资源聚合,由浅入深,由近及远:前端网