本片文章分析源码地址:
1. module机制
每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类都是私有的,对其他文件不可见。
global对象是全局对象(通过global可以实现多文件共享)。
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,第一次加载会进行缓存,之后再次加载会从缓存读取(缓存可以删除)。
- 模块加载的顺序,按照其在代码中出现的顺序。
1.1 module成员
// module构造函数
function Module(id = '', parent) {
this.id = id;
this.path = path.dirname(id);
this.exports = {};
this.parent = parent;
updateChildren(parent, this, false);
this.filename = null;
this.loaded = false;
this.children = [];
}
const module = new Module(filename, parent);
module对象代表当前模块,成员属性:
Module {
id: '.',
exports: {},
parent: null,
filename: '/Users/tanghc/Desktop/demo.js',
loaded: false,
children: [],
paths:
[ '/Users/tanghc/Desktop/node/node_modules',
'/Users/tanghc/Desktop/node_modules',
'/Users/tanghc/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
- id:模块的标识符,通常是带有绝对路径的模块文件名。
- filename: 模块文件名,带有绝对路径
- loaded:表示当前模块是否已经完成加载
- parent:表示调用当前该模块的模块(node xxx.js时为null)
- children:表示该模块用到的其他模块
- exports:对外输出的值
1.2 如何删除缓存模块:
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
2.require
2.1模块加载流程
require -> _load() -> tryModuleLoad() -> Module.prototype.load -> Module._extensions[extension](this, filename) -> _compile
2.1.1 require
函数在node内部是如何定义的?
// Loads a module at the given file path. Returns that module's
// `exports` property.
Module.prototype.require = function(id) {
validateString(id, 'id');
if (id === '') {
throw new ERR_INVALID_ARG_VALUE('id', id,
'must be a non-empty string');
}
...
try {
// require函数最终调用了_load函数
return Module._load(id, this, /* isMain */ false);
} finally {
...
}
};
2.1.2 Module._load加载文件
// request:require传入的模块路径
// parent:node调用时为null,require时指向父实例
// isMain:node调用时为true,require加载时为false
Module._load = function(request, parent, isMain) {
if (parent) {
// 看是否存在缓存路径
// 存在则直接读取,而不需要在执行一次路径解析的逻辑
// \x00 -> null,相当于空字符串
relResolveCacheIdentifier = `${parent.path}\x00${request}`;
const filename = relativeResolveCache[relResolveCacheIdentifier];
if (filename !== undefined) {
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
delete relativeResolveCache[relResolveCacheIdentifier];
}
}
// 获取文件的绝对路径
// C:/a/b/c.js
const filename = Module._resolveFilename(request, parent, isMain);
// 先从_cache缓存读取模块
const cachedModule = Module._cache[filename];
if (cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
return cachedModule.exports;
}
// 未命中缓存后从native模块查找
// 如果是navtive模块则加载
const mod = NativeModule.map.get(filename);
if (mod && mod.canBeRequiredByUsers) {
debug('load native module %s', request);
return mod.compileForPublicLoader(experimentalModules);
}
// 初始化一个新的模块
const module = new Module(filename, parent);
// 当前加载模块为主模块
if (isMain) {
process.mainModule = module;
// 主模块路径为'.'
module.id = '.';
}
// 缓存新构建的模块
Module._cache[filename] = module;
if (parent !== undefined) {
// 缓存加载路径
// 二次加载时,先根据相对路径去加载缓存(加快同目录下的模块的加载速度),未命中则调用_resolveFilename计算路径再加载。
relativeResolveCache[relResolveCacheIdentifier] = filename;
}
// 加载模块
module.load(filename);
// 返回module.exports导出的内容
return module.exports;
};
根据对源码对分析,可以得出_load的方法的总结:
- load方法会根据传入的路径来查找路径模块。
- 先尝试根据相对路径查找同文件夹下的缓存模块
- 再对路径进行解析,查找缓存模块
- 构建新模块,并将模块进行缓存
- 返回模块导出对内容。(注意返回的是
module.exports
)
2.1.3 _resolveFilename
获取文件的大致路径列表
在_load
方法内部使用Module._resolveFilename
来查找模块的真实路径,我们来看一下具体内容:
Module._resolveFilename = function(request, parent, isMain, options) {
// 原生模块直接返回加载路径(通常是模块名)
if (NativeModule.canBeRequiredByUsers(request)) {
return request;
}
if (typeof options === 'object' && options !== null &&
...
} else {
// 获取模块的大致路径
paths = Module._resolveLookupPaths(request, parent, true);
}
// Look up the filename first, since that's the cache key.
const filename = Module._findPath(request, paths, isMain);
...
return filename
}
// 查找文件可能出现的所有位置(模块的大致路径)
Module._resolveLookupPaths = function(request, parent) {
// require('moduleA')
// modulePaths是根据NODE_PATH得到的路径
// 默认从node_modules中加载依赖
if (request.charAt(0) !== '.' ||
(request.length > 1 &&
request.charAt(1) !== '.' &&
request.charAt(1) !== '/' &&
(!isWindows || request.charAt(1) !== '\\'))) {
let paths = modulePaths;
if (parent != null && parent.paths && parent.paths.length) {
/**
* parent.paths:
* /Users/tanghc/Desktop/node/node_modules
* /Users/tanghc/Desktop/node_modules
* ...
* /node_modules
*
* modulePaths:
* '/Users/tanghc/.node_modules',
* '/Users/tanghc/.node_libraries',
* '/usr/local/lib/node'
*/
paths = parent.paths.concat(paths);
}
return paths.length > 0 ? paths : null;
}
// 通过node指令方式调用
// 返回当前程序的执行路径
if (!parent || !parent.id || !parent.filename) {
const mainPaths = ['.'].concat(Module._nodeModulePaths('.'), modulePaths);
return mainPaths;
}
// 相对或者绝对路径引用的模块
// 返回其父文件夹路径
const parentDir = [path.dirname(parent.filename)];
return parentDir;
}
_resolveLookupPaths
:可能的路径以当前文件夹,nodejs系统文件夹和node_module
中的文件夹为候选,以上述顺序找到任意一个,就直接返回。注意的是,自定义模块大致路径只有一条,而第三方模块,比如node_modules
下的模块,会返回一个路径数组。如代码注释。
2.1.4 _findPath
补全路径,获取绝对(或真实)路径
// _resolveLookupPaths获取到大致路径后,_findPath负责解析到具体文件并补全后缀。
// 我们来看一下具体到补全策略
Module._findPath = function(request, paths, isMain) {
...
var exts;
// 是否以/结尾
var trailingSlash = request.length > 0 &&
request.charCodeAt(request.length - 1) === CHAR_FORWARD_SLASH;
if (!trailingSlash) {
// 是否以/.或者.结尾
trailingSlash = /(?:^|\/)\.?\.$/.test(request);
}
// 循环大致路径的list
// 直到找到真实文件
for (var i = 0; i < paths.length; i++) {
// Don't search further if path doesn't exist
const curPath = paths[i];
if (curPath && stat(curPath) < 1) continue;
// 将大致路径与require路径拼接得到可能路径
var basePath = path.resolve(curPath, request);
var filename;
// 首先判断文件类型
// 分为非文件类型和文件类型
var rc = stat(basePath);
// 不以/或者/.或者.结尾
// 比如require('main') 可能为main.js或者main/
// 可能是文件,也可能是文件夹
if (!trailingSlash) {
// 真实文件
if (rc === 0) { // File.
...
// 文件返回真实路径
filename = toRealPath(basePath);
}
// 拼接后缀
if (!filename) {
// Try it with each of the extensions
// .js,.json,.node,.mjs
if (exts === undefined)
exts = Object.keys(Module._extensions);
filename = tryExtensions(basePath, exts, isMain);
}
}
// 文件夹
if (!filename && rc === 1) { // Directory.
// try it with each of the extensions at "index"
if (exts === undefined)
exts = Object.keys(Module._extensions);
// 1.如果没有package.json或者未定义main字段,则找是否有index文件
// 2.获取main字段定义的路径,无后缀补全后缀
// 3.若补全后缀仍不存在,则加上index再补全后缀
filename = tryPackage(basePath, exts, isMain, request);
}
// 缓存路径
if (filename) {
Module._pathCache[cacheKey] = filename;
return filename;
}
}
return false;
};
从_findPath
方法来看,它需要针对文件夹和非文件夹区分不同的补全策略
- 文件类型:直接返回文件的路径
- 文件夹类型:
- 文件夹下是否有
package.json
文件,并获取文件中的main
字段定义的路径。 - 如果main字段对应的路径是一个文件且存在,那么就返回这个路径。
- main字段对应的路径对应没有带后缀,那么尝试使用.js,.json,.node,.ms后缀去加载对应文件
- 如果以上2个条件都不满足,那么尝试对应路径下的index.js,index.json,index.node文件。
- 如果未定义main字段,那么就对文件路径后添加分别添加.js,.json,.node,.ms后缀去加载对应的文件。
- 找到则缓存路径,未找到抛出错误。
- 文件夹下是否有
_findPath
路径补全的优先级:
- 具体的文件
- 若文件无后缀,则补全后缀(顺序:.js,.json,.node,.mjs)
- 通过
package.json main
补全的路径 - 查找index文件并加上后缀的路径
2.1.5 module.load
通过以上步骤,node已经找到的具体的模块,接下便进入最后的阶段:即读取模块内容,编译模块再执行具体的内部逻辑。
Module.prototype.load = function(filename) {
...
this.filename = filename;
...
// 获取后缀,即上面列出的4个
// 如果不是给定后缀,则按.js后缀处理(包括未给定后缀的文件)
const extension = findLongestRegisteredExtension(filename);
// 开始load该文件
Module._extensions[extension](this, filename);
this.loaded = true;
};
node
在内部对文件对后缀处理上,默认会按.js
后缀文件处理。这也就是说,当加载的文件无后缀或者后缀不在.js,.json,.node,.ms
之内时,也会按.js
文件去处理。
2.1.6 加载编译文件
我们主要来看一下node
内部是如何加载解析js
文件的:
Module._extensions['.js'] = function(module, filename) {
const content = fs.readFileSync(filename, 'utf8');
module._compile(content, filename);
};
首先将文件内容按utf8
格式读取到内存。
Module.prototype._compile = function(content, filename) {
// const compiledWrapper = wrapSafe(filename, content);
// 将文件内容包装在一个自执行的函数内
const wrapper = Module.wrap(content);
compiledWrapper = vm.runInThisContext(wrapper, {
filename,
lineOffset: 0,
displayErrors: true,
importModuleDynamically: experimentalModules ? async (specifier) => {
const loader = await asyncESM.loaderPromise;
return loader.import(specifier, normalizeReferrerURL(filename));
} : undefined,
});
...
// 加载的文件所在的目录名
const dirname = path.dirname(filename);
// require方法
const require = makeRequireFunction(this);
//
var result;
// module.exports
const exports = this.exports;
const thisValue = exports;
const module = this;
if (requireDepth === 0) statCache = new Map();
...
// thisValue: module.exports的引用。thisValue = module.exports。模块内部this默认指向module.exports
// exports: module.exports的引用
// require: require方法
// module:Module实例
// filename:文件的绝对路径
// dirname:文件所在文件夹的绝对路径
result = compiledWrapper.call(thisValue, exports, require, module,
filename, dirname);
...
if (requireDepth === 0) statCache = null;
return result;
};
在编译执行文件内容时,node
会先把文件内容包装在一个函数内,对应Module.wrap
方法,包装结果:
function (exports, require, module, __filename, __dirname) {
// 文件内容content
}
所以,我们在文件内部可以直接拿到函数传入的参数,而无需声明。包装完毕后,调用vm.runInThisContext
为代码创建一个独立沙箱运行空间,包装的代码可以访问外部的global
对象,但不能访问其他沙箱内运行的变量。返回包装后的函数。
最好调用包装好的函数,并注入对应的参数。总体过程:
- 通过
wrapSafe
方法将内容包装在在一个独立的沙箱运行环境内,返回包装的函数。 - 调用包装函数并注入对应参数。
这里在看一下注入的相关参数:
thisValue
:包装函数运行的上下文,即模块内部this默认指向module.exports
。exports
:module.exports
的引用。require
:require
方法。module
:模块Module
实例。filename
(__filename):文件的绝对路径。dirname
(__dirname):文件所在文件夹的绝对路径。
可以看到module.exports与exports共享一块内容。
到这里,node
已经完成来指定模块的加载与执行,但光这些还不够,我们还需要拿到指定模块对外暴露的内容(即存放在module.exports
或exports
)中的内容。我们再回到2.1.2 Module._load函数结尾
,该函数的返回值即模块对外的暴露值:
return module.exports;
最终暴露的是module.exports
中挂载的属性。
Module._compile方法是同步执行的,所以Module._load要等它执行完成,才会向用户返回module.exports的值。
2.1.7 require辅助方法
在_compile
函数结尾,发现通过makeRequireFunction
返回来一个require
函数,而不是直接使用this.require
方法,这是为什么呢?来看一下方法的具体内容:
// mod就是传入的Module构造方法的原型对象
function makeRequireFunction(mod) {
const Module = mod.constructor;
// 声明一个新的require方法
function require(path) {
return mod.require(path);
}
// resolve方法同_resolveFilename方法,获取加载模块的真实路径。
// 比如require.resolve('./main') main文件夹下存在index.js模块
// 返回/Users/用户名/Desktop/node/main/index.js
function resolve(request, options) {
validateString(request, 'request');
return Module._resolveFilename(request, mod, false, options);
}
require.resolve = resolve;
// 获取模块的所有大致路径
function paths(request) {
validateString(request, 'request');
return Module._resolveLookupPaths(request, mod);
}
resolve.paths = paths;
require.main = process.mainModule;
// Enable support to add extra extension types.
require.extensions = Module._extensions;
require.cache = Module._cache;
return require;
}
makeRequireFunction
方法对require
函数进行来一次包装,增加了一些辅助函数和属性:
- require.resolve():将模块名解析到一个绝对路径
- require.main:指向主模块(入口模块)
- require.cache:缓存模块
- require.extensions:根据文件后缀加载不同函数,
{ '.js': [Function], '.json': [Function], '.node': [Function] }
从模块加载机制看一些常见问题
node如何处理循环依赖
当main.js加载a.js时,a.js又加载b.js,b.js又尝试去加载a.js。为了防止无限的循环,会返回一个a.js的exports对象的未完成副本给b.js模块,然后b.js完成加载,并将exports对象提供给a.js模块。
// module.js
module.exports.a = 1
require('./subModule')
module.exports.b = 2
// subModule.js
const parent = require('./module')
console.log(parent.a)
console.log(parent.b)
module.exports = {
getB() { console.log(parent.b) }
}
// main.js
const a = require('./module')
const b = require('./subModule')
b.getB()
// output
// 1
// undefined
// 2
值传递与引用传递
如果对外输出的是一个值,那么输出的值是内部输入值的一个拷贝,即内部的变化不会影响对外输出的值。引用传递除外:
// count.js
let obj = {
num: 1
}
function incCounter() {
obj.num += 1
}
module.exports = {
obj,
incCounter
}
// main.js
const { obj, incCounter } = require("./count")
console.log('%o', obj);
incCounter()
console.log('%o', obj);
// { num: 1 }
// { num: 2 }
EsModule
与CommonJs
export default
default是ES6引入的与export配套使用的关键字,用来给匿名对象、匿名函数设置默认的名字用的
export出来的值必须要有一个命名,否则从语法层次便会报错
文章内容如有错误,欢迎指证!
转载请注明出处!非常感谢!