Node require 学习笔记
date
Feb 27, 2023
slug
node-require
status
Published
tags
Nodejs
summary
分析 require 模块加载机制
type
Post
require
的使用场景
- 加载模块类型:
- 内置模块:
require('fs')
- node_modules 依赖:
require('lodash')
- 开发模块:
require('./utils')
- 支持加载的文件类型:
- .js
- .node(调用
process.dlopen(module, path.toNamespacedPath(filename))
处理) - .json
- .mjs
- 其他类型(均被当作 .js 处理)
除了
.node
文件,其余文件均使用 fs.readFileSync
读取文件内容然后构造可执行的 compiledWrapper
函数处理。require
执行流程分析
流程图示
require module
对象属性记录
id
: 源码文件路径,如:/Users/i7eo/Documents/Workspace/GitHub/eopol/cli-helper-test/src/require-orignal.js
根Module
id
为 '.'
path
: 源码文件所在的文件夹,通过path.dirname(id)
生成,例如:/Users/i7eo/Documents/Workspace/GitHub/eopol/cli-helper-test/src
根据这个向上递归生成一级一级的目录(paths)用来寻找node_modules
最上层为/node_modules
exports
: 模块输出的内容,默认为{}
parent
: 父模块信息
filename
: 源码文件路径
loaded
: 是否已经加载完毕
children
: 子模块对象集合
paths
: 模块查询范围
流程详细描述
require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
加载主流程,runMain
中判断当前是 cjs 还是 esm
- require 加载路径必为 string。
validateString(id, 'id');
Module._load(process.argv[1], null, true)
加载当前文件(主模块,第二个参数 parent 为 null,第三个参数 ismain 为 true)
Module._resolveFilename
根据 paths、ext(.js/.json/…)、packageManager 拼出绝对路径
Module._cache[filename]
查看是否有缓存,如果有即刻返回
- 通过
loadBuiltinModule
带着上面拼出的绝对路径去内置库找(BuiltinModule.map
存储所有内置模块,已 _ 开头)如果有即刻返回
- 如果不是内置模块,通过 new Module 创建实例,并且把创建的实例更新至 parent 下的 children 中
- 将创建的实例进行缓存,key 为文件绝对路径
- 调用
module.load
加载具体文件
- 通过
findLongestRegisteredExtension
找到当前文件的解析器(Module._extensions['.js']
/Module._extensions['.node']
/Module._extensions['.json']
),并调用读取内容(fs.readFileSync
)以及生成可以执行函数(module._compile
其中调用compileFunction
将读取的内容以及 exports, require, module, filename, dirname 传入,生成一个可执行函数compiledWrapper
)
- 在
_compile
方法底部调用compiledWrapper
并将结果返回
以上为执行
node ./src/require-orignal.js
文件内容前主流程的过程。上述过程会先于文件内容执行,文件内容中的 require 断点直接进入下面 12- 因为主流程已走完,require 方法已经当作参数传入当前文件所以直接调用
require('xxx')
会直接进入Module.prototype.require
,该方法加载传入的指定路径文件(ismain 为 false),其中利用requireDepth
加减来控制是否加载完毕,文件中的依赖也是通过调用Module.prototype.require
加载。调用Module._load
重复上述 3-11 的过程
杂项记录
require
连续加载同一个模块时,是如何进行缓存的?- 两个缓存
- 父
module
的path
+\x00
文件名对应的绝对路径+文件名.后缀 - 绝对路径+文件名.后缀,对应的
Module
对象
path.dirname(filename)
获取文件路径
path.basename(filename)
获取文件名(带后缀)
- 每需要一次
require
requireDepth
加一,执行完require
进来的模块后,requireDepth
减一
Module._resolveFilename()
调用Module._findPath()
之后,在返回的文件名不存在,没有模块,报错
Module._nodeModulePaths(path.dirname(filename))
产生的node_modules
数组,最终保存在了Module
对象上
- internal/modules/cjs/loader.js findLongestRegisteredExtension(filename) 获取 扩展名,默认返回 js
- path.sep 路径分隔符 Mac: '/' window: '\'
- '#!' 名字是 shebang
- 加载内置模块和其他模块的
compileFunction
方法来源模块是不一样的
思考
- commonjs 加载主模块流程?
流程详细描述 1-11
- commonjs 如何加载内置模块?
流程详细描述 1-6
- commonjs 如何加载 node_moduls 模块?同12-3-11