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 执行流程分析

流程图示

notion image

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 : 模块查询范围

流程详细描述

  1. require('internal/modules/cjs/loader').Module.runMain(process.argv[1]); 加载主流程,runMain 中判断当前是 cjs 还是 esm
  1. require 加载路径必为 string。validateString(id, 'id');
  1. Module._load(process.argv[1], null, true) 加载当前文件(主模块,第二个参数 parent 为 null,第三个参数 ismain 为 true)
  1. Module._resolveFilename 根据 paths、ext(.js/.json/…)、packageManager 拼出绝对路径
  1. Module._cache[filename] 查看是否有缓存,如果有即刻返回
  1. 通过 loadBuiltinModule 带着上面拼出的绝对路径去内置库找(BuiltinModule.map 存储所有内置模块,已 _ 开头)如果有即刻返回
  1. 如果不是内置模块,通过 new Module 创建实例,并且把创建的实例更新至 parent 下的 children 中
  1. 将创建的实例进行缓存,key 为文件绝对路径
  1. 调用 module.load 加载具体文件
  1. 通过 findLongestRegisteredExtension 找到当前文件的解析器(Module._extensions['.js']/Module._extensions['.node']/Module._extensions['.json']),并调用读取内容(fs.readFileSync)以及生成可以执行函数(module._compile 其中调用 compileFunction 将读取的内容以及 exports, require, module, filename, dirname 传入,生成一个可执行函数 compiledWrapper
  1. _compile 方法底部调用 compiledWrapper 并将结果返回
💡
以上为执行 node ./src/require-orignal.js 文件内容前主流程的过程。上述过程会先于文件内容执行,文件内容中的 require 断点直接进入下面 12
  1. 因为主流程已走完,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 方法来源模块是不一样的

思考

  1. commonjs 加载主模块流程?
    1. 流程详细描述 1-11
  1. commonjs 如何加载内置模块?
    1. 流程详细描述 1-6
  1. commonjs 如何加载 node_moduls 模块?同12-3-11

参考资料


© i7eo 2017 - 2024