总结
要获取调用 require()
时将加载的确切文件名,则使用 require.resolve()
函数。
综上所述,这里是 require()
的伪代码高级算法:
require(X) from module at path Y
1.
2.
3.
4.
5.
6.
7.
LOAD_AS_FILE(X)
1.
2.
3.
4.
LOAD_INDEX(X)
1.
2.
3.
LOAD_AS_DIRECTORY(X)
1.
2. LOAD_INDEX(X)
LOAD_NODE_MODULES(X, START)
1.
2.
NODE_MODULES_PATHS(START)
1.
2.
3.
4.
5.
LOAD_PACKAGE_IMPORTS(X, DIR)
1.
2.
3.
4.
5.
LOAD_PACKAGE_EXPORTS(X, DIR)
1.
2.
3.
4.
5.
6. RESOLVE_ESM_MATCH(MATCH)
LOAD_PACKAGE_SELF(X, DIR)
1.
2.
3.
4.
5.
6. RESOLVE_ESM_MATCH(MATCH)
RESOLVE_ESM_MATCH(MATCH)
1.
2.
3.
4.
5.
## Caching
<!--type=misc-->
模块在第一次加载后被缓存。
这意味着(类似其他缓存)每次调用 `require('foo')` 都会返回完全相同的对象(如果解析为相同的文件)。
如果 `require.cache` 没有被修改,则多次调用 `require('foo')` 不会导致模块代码被多次执行。
这是重要的特征。
有了它,可以返回“部分完成”的对象,从而允许加载传递依赖项,即使它们会导致循环。
要让模块多次执行代码,则导出函数,然后调用该函数。
### Module caching caveats
<!--type=misc-->
模块根据其解析的文件名进行缓存。
由于模块可能会根据调用模块的位置(从 `node_modules` 文件夹加载)解析为不同的文件名,因此如果 `require('foo')` 解析为不同的文件,则不能保证 `require('foo')` 将始终返回完全相同的对象。
此外,在不区分大小写的文件系统或操作系统上,不同的解析文件名可以指向同一个文件,但缓存仍会将它们视为不同的模块,并将多次重新加载文件。
例如,`require('./foo')` 和 `require('./FOO')` 返回两个不同的对象,而不管 `./foo` 和 `./FOO` 是否是同一个文件。
## Core modules
<!--type=misc-->
Node.js 有些模块编译成二进制文件。
这些模块在本文档的其他地方有更详细的描述。
核心模块在 Node.js 源代码中定义,位于 `lib/` 文件夹中。
如果将核心模块的标识符传给 `require()`,则始终优先加载核心模块。
例如,`require('http')` 将始终返回内置的 HTTP 模块,即使存在该名称的文件。
## Cycles
<!--type=misc-->
当有循环 `require()` 调用时,模块在返回时可能尚未完成执行。
考虑这种情况:
`a.js`:
```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
需要仔细规划以允许循环模块依赖项在应用程序中正常工作。
To get the exact filename that will be loaded when require()
is called, use
the require.resolve()
function.
Putting together all of the above, here is the high-level algorithm
in pseudocode of what require()
does:
require(X) from module at path Y
1. If X is a core module,
a. return the core module
b. STOP
2. If X begins with '/'
a. set Y to be the filesystem root
3. If X begins with './' or '/' or '../'
a. LOAD_AS_FILE(Y + X)
b. LOAD_AS_DIRECTORY(Y + X)
c. THROW "not found"
4. If X begins with '#'
a. LOAD_PACKAGE_IMPORTS(X, dirname(Y))
5. LOAD_PACKAGE_SELF(X, dirname(Y))
6. LOAD_NODE_MODULES(X, dirname(Y))
7. THROW "not found"
LOAD_AS_FILE(X)
1. If X is a file, load X as its file extension format. STOP
2. If X.js is a file, load X.js as JavaScript text. STOP
3. If X.json is a file, parse X.json to a JavaScript Object. STOP
4. If X.node is a file, load X.node as binary addon. STOP
LOAD_INDEX(X)
1. If X/index.js is a file, load X/index.js as JavaScript text. STOP
2. If X/index.json is a file, parse X/index.json to a JavaScript object. STOP
3. If X/index.node is a file, load X/index.node as binary addon. STOP
LOAD_AS_DIRECTORY(X)
1. If X/package.json is a file,
a. Parse X/package.json, and look for "main" field.
b. If "main" is a falsy value, GOTO 2.
c. let M = X + (json main field)
d. LOAD_AS_FILE(M)
e. LOAD_INDEX(M)
f. LOAD_INDEX(X) DEPRECATED
g. THROW "not found"
2. LOAD_INDEX(X)
LOAD_NODE_MODULES(X, START)
1. let DIRS = NODE_MODULES_PATHS(START)
2. for each DIR in DIRS:
a. LOAD_PACKAGE_EXPORTS(X, DIR)
b. LOAD_AS_FILE(DIR/X)
c. LOAD_AS_DIRECTORY(DIR/X)
NODE_MODULES_PATHS(START)
1. let PARTS = path split(START)
2. let I = count of PARTS - 1
3. let DIRS = [GLOBAL_FOLDERS]
4. while I >= 0,
a. if PARTS[I] = "node_modules" CONTINUE
b. DIR = path join(PARTS[0 .. I] + "node_modules")
c. DIRS = DIRS + DIR
d. let I = I - 1
5. return DIRS
LOAD_PACKAGE_IMPORTS(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "imports" is null or undefined, return.
4. let MATCH = PACKAGE_IMPORTS_RESOLVE(X, pathToFileURL(SCOPE),
["node", "require"]) defined in the ESM resolver.
5. RESOLVE_ESM_MATCH(MATCH).
LOAD_PACKAGE_EXPORTS(X, DIR)
1. Try to interpret X as a combination of NAME and SUBPATH where the name
may have a @scope/ prefix and the subpath begins with a slash (`/`).
2. If X does not match this pattern or DIR/NAME/package.json is not a file,
return.
3. Parse DIR/NAME/package.json, and look for "exports" field.
4. If "exports" is null or undefined, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(DIR/NAME), "." + SUBPATH,
`package.json` "exports", ["node", "require"]) defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)
LOAD_PACKAGE_SELF(X, DIR)
1. Find the closest package scope SCOPE to DIR.
2. If no scope was found, return.
3. If the SCOPE/package.json "exports" is null or undefined, return.
4. If the SCOPE/package.json "name" is not the first segment of X, return.
5. let MATCH = PACKAGE_EXPORTS_RESOLVE(pathToFileURL(SCOPE),
"." + X.slice("name".length), `package.json` "exports", ["node", "require"])
defined in the ESM resolver.
6. RESOLVE_ESM_MATCH(MATCH)
RESOLVE_ESM_MATCH(MATCH)
1. let { RESOLVED, EXACT } = MATCH
2. let RESOLVED_PATH = fileURLToPath(RESOLVED)
3. If EXACT is true,
a. If the file at RESOLVED_PATH exists, load RESOLVED_PATH as its extension
format. STOP
4. Otherwise, if EXACT is false,
a. LOAD_AS_FILE(RESOLVED_PATH)
b. LOAD_AS_DIRECTORY(RESOLVED_PATH)
5. THROW "not found"