nodejs模块引入机制exports&module源码

一段报错信息

我们可以通过下面的一段报错信息,了解nodejs整个执行过程。我将根据整个执行过程,来进行源码的讲解,为源码添加大量注释来达到直观效果。

1
2
3
4
5
6
7
8
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

bootstrap_node载入Module

bootstrap_node(bootstrap_node源码)
文件是nodejs第一个js执行文件,通过node.cc文件执行。
startup函数通过如下源码,去加载module模块:

1
2
3
4
5
//加载module模块,原生模块都通过NativeModule的require加载
const Module = NativeModule.require('module');
....
//加载主文件,也就是你们的入口文件
Module.runMain();

NativeModule

NativeModule是加载,生成原生模块实例,编译,执行原生模块的函数,源码在bootstrap_node文件中,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
function NativeModule(id) {
this.filename = `${id}.js`;
this.id = id;
this.exports = {};
this.loaded = false;
this.loading = false;
}
NativeModule._source = process.binding('natives');
NativeModule._cache = {};
const config = process.binding('config');
NativeModule.require = function(id) {
//如果是native_module,直接返回
if (id === 'native_module') {
return NativeModule;
}
//如果有缓存,并且编译过或正在编译,直接返回
const cached = NativeModule.getCached(id);
if (cached && (cached.loaded || cached.loading)) {
return cached.exports;
}
//如果不存在这个原生模块,直接报错
if (!NativeModule.exists(id)) {
const err = new Error(`No such built-in module: ${id}`);
err.code = 'ERR_UNKNOWN_BUILTIN_MODULE';
err.name = 'Error [ERR_UNKNOWN_BUILTIN_MODULE]';
throw err;
}
//将原生模块push到moduleLoadList数组中
process.moduleLoadList.push(`NativeModule ${id}`);
//创建一个原生模块
const nativeModule = new NativeModule(id);
//将模块添加到缓存中
nativeModule.cache();
//执行编译,关键代码
nativeModule.compile();
//返回nativeModule.exports
//nativeModule.exports的赋值在模块文件中完成。
return nativeModule.exports;
};
//获取缓存
NativeModule.getCached = function(id) {
return NativeModule._cache[id];
};
//原生模块是否纯在
NativeModule.exists = function(id) {
return NativeModule._source.hasOwnProperty(id);
};
//获取模块内容信息
NativeModule.getSource = function(id) {
return NativeModule._source[id];
};
//将模块的内容包裹一层函数
//Module.js文件源码中:
//Module.wrapper = NativeModule.wrapper;
//Module.wrap = NativeModule.wrap;
//所以除了NativeModule,Module也具有wrapper,wrap函数
//这就是为什么我们可以在文件中直接使用exports,require,__filename的原因
NativeModule.wrap = function(script) {
return NativeModule.wrapper[0] + script + NativeModule.wrapper[1];
};
//包裹的函数
NativeModule.wrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
];
//编译
NativeModule.prototype.compile = function() {
//获取原生模块中代码
var source = NativeModule.getSource(this.id);
//进行处理
source = NativeModule.wrap(source);
this.loading = true;
try {
//runInThisContext vm虚拟机知识,可以去了解vm
//创建独立的沙箱运行空间,执行source字符串,fn变量为执行后返回的内容
const fn = runInThisContext(source, {
filename: this.filename,
lineOffset: 0,
displayErrors: true
});
//将模块文件中使用的变量,进行传入。
//所以模块文件中的exports为module.exports的引用。
//require为 NativeModule.require (只是原生模块,其他模块这里不一样)。
fn(this.exports, NativeModule.require, this, this.filename);
//编译完成
this.loaded = true;
} finally {
this.loading = false;
}
};
//缓存
NativeModule.prototype.cache = function() {
NativeModule._cache[this.id] = this;
};

通过上面源码可以看出const Module = NativeModule.require('module'),Module其实是新创建的原生模块实例的exports属性返回值。
而module.js文件中代码可以使用这个新创建的原生模块实例所有属性。

Module源码

根据代码的调用顺序进行,源代码的解析:

runMain

bootstrap_node文件中执行Module.runMain(),Module模块runMain源码如下:

1
2
3
4
5
6
Module.runMain = function() {
//process.argv[1]命令中第二个参数,一般为入口文件名称
Module._load(process.argv[1], null, true);
//
process._tickCallback();
}

_load

runMain执行了_load这个函数,_load源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
//request:入口文件
//parent:导入了当前模块的模块,也就是说当前使用require的文件模块的实例化。
//isMain:是否为主文件
Module._load = function(request, parent, isMain) {
if (parent) {
debug('Module._load REQUEST %s parent: %s', request, parent.id);
}
var filename = null;
//为主函数
if (isMain) {
try {
//获取文件名称,module.js文件中的代码太多,只展示主要的。
filename = Module._resolveFilename(request, parent, isMain);
} catch (e) {
// try to keep stack
e.stack;
throw e;
}
//experimentalModules实验性模块
//比如:使用 ES Module时,`import xxx form `xxxx`。需要标识为experimentalModules。(nodejs 8.5)
if (experimentalModules) {
(async () => {
// loader setup
if (!ESMLoader) {
ESMLoader = new Loader();
const userLoader = process.binding('config').userLoader;
if (userLoader) {
const hooks = await new Loader().import(userLoader);
ESMLoader.hook(hooks);
}
}
await ESMLoader.import(getURLFromFilePath(filename).href);
})()
.catch((e) => {
console.error(e);
process.exit(1);
});
return;
}
} else {
//获取文件名称
filename = Module._resolveFilename(request, parent, isMain);
}
//如果缓存中已经具有当前模块就返回模块
var cachedModule = Module._cache[filename];
if (cachedModule) {
//因为其他文件也许引入了该模块,所以缓存中具有改模块,但是当前引入该模块的父亲模块的parent.children中,并不一定具有该模块。
//所以更新parent.children, 将当前模块加入带parent.children中
updateChildren(parent, cachedModule, true);
//返回模块的exports
return cachedModule.exports;
}
//如果模块为原生模块,需要使用NativeModule的require来加载
if (NativeModule.nonInternalExists(filename)) {
debug('load native module %s', request);
return NativeModule.require(filename);
}
//创建个模块实例
// Don't call updateChildren(), Module constructor already does.
var module = new Module(filename, parent);
//主模块
if (isMain) {
process.mainModule = module;
module.id = '.';
}
//添加缓存
Module._cache[filename] = module;
//执行tryModuleLoad
tryModuleLoad(module, filename);
//返回exports
return module.exports;
}

load

tryModuleLoad函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function tryModuleLoad(module, filename) {
var threw = true;
try {
//执行 load
module.load(filename);
threw = false;
} finally {
//如果报错,删除缓存
if (threw) {
delete Module._cache[filename];
}
}
}

load函数源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Module.prototype.load = function(filename) {
debug('load %j for module %j', filename, this.id);
//确定当前模块未被加载
assert(!this.loaded);
this.filename = filename;
//文件路径
this.paths = Module._nodeModulePaths(path.dirname(filename));
//文件后缀
var extension = path.extname(filename) || '.js';
//如果没有当前函数,extension设置为.js
if (!Module._extensions[extension]) extension = '.js';
//执行函数,下面有源码
Module._extensions[extension](this, filename);
//加载完成
this.loaded = true;
if (ESMLoader) {
const url = getURLFromFilePath(filename);
const urlString = `${url}`;
if (ESMLoader.moduleMap.has(urlString) !== true) {
const ctx = createDynamicModule(['default'], url);
ctx.reflect.exports.default.set(this.exports);
ESMLoader.moduleMap.set(urlString,
new ModuleJob(ESMLoader, url, async () => ctx));
} else {
const job = ESMLoader.moduleMap.get(urlString);
if (job.reflect)
job.reflect.exports.default.set(this.exports);
}
}
};

Module._extensions

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
Module._extensions['.js'] = function(module, filename) {
//后缀为.js 读取文件内容
var content = fs.readFileSync(filename, 'utf8');
//编译 (stripBOM对带BOM头的内容进行处理,这是编码方面知识)
module._compile(internalModule.stripBOM(content), filename);
};
// Native extension for .json
//后缀为.json,处理
Module._extensions['.json'] = function(module, filename) {
var content = fs.readFileSync(filename, 'utf8');
try {
module.exports = JSON.parse(internalModule.stripBOM(content));
} catch (err) {
err.message = filename + ': ' + err.message;
throw err;
}
};
//Native extension for .node
Module._extensions['.node'] = function(module, filename) {
return process.dlopen(module, path.toNamespacedPath(filename));
};
//实验性模块处理
//比如:nodejs 8.5 支持 ES Module,需修改文件扩展名应为mjs,并标识experimentalModules
if (experimentalModules) {
Module._extensions['.mjs'] = function(module, filename) {
throw new errors.Error('ERR_REQUIRE_ESM', filename);
};
}

_compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
Module.prototype._compile = function(content, filename) {
content = internalModule.stripShebang(content);
// create wrapper function
//Module.wrap等于 NativeModule.wrap 之前的注释里面也有
var wrapper = Module.wrap(content);
//创建独立的沙箱运行空间,并执行wrapper
//vm.runInThisContext中的wrapper可以直接使用global,这就是为什么我们可以直接使用global的原因
var compiledWrapper = vm.runInThisContext(wrapper, {
filename: filename,
lineOffset: 0,
displayErrors: true
});
var inspectorWrapper = null;
if (process._breakFirstLine && process._eval == null) {
if (!resolvedArgv) {
// we enter the repl if we're not given a filename argument.
if (process.argv[1]) {
resolvedArgv = Module._resolveFilename(process.argv[1], null, false);
} else {
resolvedArgv = 'repl';
}
}
// Set breakpoint on module start
if (filename === resolvedArgv) {
delete process._breakFirstLine;
inspectorWrapper = process.binding('inspector').callAndPauseOnStart;
if (!inspectorWrapper) {
const Debug = vm.runInDebugContext('Debug');
Debug.setBreakPoint(compiledWrapper, 0, 0);
}
}
}
var dirname = path.dirname(filename);
var require = internalModule.makeRequireFunction(this);
var depth = internalModule.requireDepth;
if (depth === 0) stat.cache = new Map();
var result;
if (inspectorWrapper) {
result = inspectorWrapper(compiledWrapper, this.exports, this.exports,
require, this, filename, dirname);
} else {
//将this.exports作为运行函数的作用域,将新建模块的属性作为参数传
//这就是为什么,模块中this等于module.exports的原因
result = compiledWrapper.call(this.exports, this.exports, require, this,
filename, dirname);
}
if (depth === 0) stat.cache = null;
return result;
}

上面的代码,我们看见了一个模块的加载和执行过程,并了解了我们能直接使用exports,require,module,__dirname等等的原因。
还有this指代module.exports的原因,下面将解释我们最常用的require的源码。

require

1
2
3
4
5
6
7
8
Module.prototype.require = function(path) {
//路径不能为空
assert(path, 'missing path');
//路径为字符串
assert(typeof path === 'string', 'path must be a string');
//执行_load
return Module._load(path, this, /* isMain */ false);
}

我们看见require的代码就短短几句话,最后返回Module._load的执行结果。而通过上面的代码解释,我们知道Module._load返回的是
我们引入的模块的实例化对象的exports属性。如果还不明白可以看下面例子。

例子讲解

例如:

1
2
3
4
5
6
//入口文件 index.js:
var add = require('./add.js');
console.log(add(1,2));
//add.js
module.exports = function(a,b){return a+b};
//执行node index.js

简单的来说,那么程序会执行。

  1. bootstrap_node的startup函数,加载Module模块。然后执行Module.runMain。
  2. Module.runMain会加载并执行主文件index.js。
  3. 主文件执行require这里的require其实就是主文件实例化后的module.require。
  4. 然后,加载并执行add.js,生成新的模块实例,执行并为add.js模块实例的属性exports赋值。
  5. 最终,将新的模块的exports属性,即module.exports,返回给add这个变量。
  6. 所以add变量的值,其实就为function(a,b){return a+b}这个函数。

参考
node源码