require() 方法详解

简介: 在 NodeJS 中有一个方法是我们使用频率最高的,那就是 require 方法。NodeJs 遵循 CommonJS 规范,该规范的核心是通过 require来加载其他依赖的模块。

在 NodeJS 中有一个方法是我们使用频率最高的,那就是 require 方法。NodeJs 遵循 CommonJS 规范,该规范的核心是通过 require来加载其他依赖的模块。


几个问题


  1. module.exports 或者 exports 是全局变量吗?
  2. 模块的加载是同步还是异步?
  3. 循环引用会不会产生性能问题或者导致错误?


什么是 CommonJS


每一个文件就是一个模块,拥有自己独立的作用域,变量,以及方法等,对其他的模块都不可见。CommonJS 规范规定,每个模块内部,module  变量代表当前模块。这个变量是一个对象,它的 exports 属性(即module.exports)是对外的接口。


Node 模块的分类


  1. build-in modules —— Nodejs 中以 C++ 形式提供的模块。
  2. constant module —— Nodejs 中定义常量的模块。
  3. native module —— Nodejs 中以 javascript 形式提供的模块。
  4. 第三方module —— 由第三方提供的模块。


module 对象


NodeJs 内部提供一个 Module 构建函数。所有模块都是 Module 的实例。


每个模块内部,都有一个 module 对象,代表当前模块。它有以下属性。


  • module 对象的属性


  • module.id 模块的识别符,通常是带有绝对路径的模块文件名。
  • module.filename 模块的文件名,带有绝对路径。
  • module.loaded 返回一个布尔值,表示模块是否已经完成加载。
  • module.parent 返回一个对象,表示调用该模块的模块(程序入口文件的module.parent为null)
  • module.children 返回一个数组,表示该模块要用到的其他模块。
  • module.exports 表示模块对外输出的值。


  • module.exports 属性


module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。module.exports属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取module.exports变量。


  • exports 变量


我们有时候会这么写:


// test.js
function test(){
    console.log(test);
}
export.test = test;
// result.js
const test = require("./test")


这样也可以拿到正确的结果,这是因为:exports 变量指向 module.exports。这等同在每个模块头部,有一行这样的命令。


var exports = module.exports;


注意:不能直接给  exports  变量赋值,这样会改变  exports 的指向,不再指向 module.exports。在其他模块使用 require 方法是拿不到赋给 exports 的值的,因为  require 方法获取的是其他模块的 module.exports 的值


建议:尽可能的使用 module.exports 来导出结果。


模块的流程


  • 创建模块
  • 导出模块
  • 加载模块
  • 使用模块


require 方法


require 是 node 用来加载并执行其它文件导出的模块的方法。

在 NodeJs 中,我们引入的任何一个模块都对应一个 Module 实例,包括入口文件。


完整步骤:

  1. 调用父模块的 require 方法(父模块是指调用模块的当前模块)


require = function require(path) {
    return mod.require(path);
};


  1. 调用 Module 的 _load 方法


  1. 通过 Module._resolveFilename 获取模块的路径 fileName


const filename = Module._resolveFilename(request, parent, isMain);


  1. 根据 fileName 判断是否存在该模块的缓存


  • 如果存在缓存,则调用 updateChildren 方法在更新缓存内容,并返回缓存
  • 如果不存在缓存,则继续执行


  1. 当做原生模块,调用 loadNativeModule 方法进行加载


  • 如果加载成功,则返回该原生模块
  • 否则,继续执行


  1. 根据当前模块名(路径)和父模块对象生成一个 Module 实例:


const module = cachedModule || new Module(filename, parent);


  1. 再判断该模块是否是入口文件


if (isMain) {
    process.mainModule = module;
    module.id = '.';
}


  1. 将该模块的实例存入到 Module 的缓存中


Module._cache[filename] = module;


9.png


  1. 该模块的实例调用自身的 load 方法,根据 fileName 加载模块


module.load(filename);


  1. 获取该模块文件的后缀名称


const extension = findLongestRegisteredExtension(filename);


如果后缀名称是ES Module格式的(.mjs),则判断Module是否支持.mjs文件的解析,如果不支持,则抛出异常。


  1. 根据后缀名称解析模块文件内容


Module._extensions[extension](this, filename);


  1. 根据fileName读取文件内容


content = fs.readFileSync(filename, 'utf8');


  1. 编译并执行读取到的文件,调用 module 自身的 _complile 方法:


module._compile(content, filename);


_compile 主要内容步骤:


const compiledWrapper = wrapSafe(filename, content, this);
const dirname = path.dirname(filename);
const require = makeRequireFunction(this, redirects);
let result;
const exports = this.exports;
const thisValue = exports;
const module = this;
result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname);
return result;


wrapSafe方法的返回值


10.png


具体获得上图结果的代码是:


const wrapper = Module.wrap(content);
return vm.runInThisContext(wrapper, {
    filename,
    lineOffset: 0,
    displayErrors: true,
    importModuleDynamically: async (specifier) => {
        const loader = asyncESM.ESMLoader;
        return loader.import(specifier, normalizeReferrerURL(filename));
    },
});


  1. 修改该模块的加载状态为true


this.loaded = true;


  1. 加载成功。


总结


通过上面的调试过程可得出以下结论:


  1. 在NodeJs中,从入口文件开始,一切皆 Module
  2. 模块的加载是同步的。
  3. 由于缓存机制的存在,模块的循环引用对性能的影响微乎其微,并且循环引用到的模块可能是不完整的,并且可能会导致错
  4. require 查找模块的流程如下:


11.png


  1. 文件路径的解析流程图如下:


12.png


本文完


学习有趣的知识,结识有趣的朋友,塑造有趣的灵魂!


知识与技能并重,内力和外功兼修,理论和实践两手都要抓、两手都要硬!




相关文章
|
6月前
|
JavaScript 前端开发
JS require 与 import 的区别
JS require 与 import 的区别
182 1
|
6月前
|
JavaScript 前端开发
JS中 require 与 import 的区别
JS中 require 与 import 的区别
|
28天前
|
JavaScript 前端开发
require
【10月更文挑战第24天】
26 5
|
6月前
nrm 安装后报错 Error [ERR_REQUIRE_ESM]: require() of ES Module
nrm 安装后报错 Error [ERR_REQUIRE_ESM]: require() of ES Module
603 0
|
JavaScript
js文件中的require以及import 等语句中的{ }的作用
js文件中的require以及import 等语句中的{ }的作用
|
JavaScript
vite无法使用require,require is not defined
vite无法使用require,require is not defined
540 0
|
C语言
函数 require
函数 require
104 0
|
缓存 JSON 移动开发
深入解析require源码,知其根,洞其源
深入解析require源码,知其根,洞其源
424 1
深入解析require源码,知其根,洞其源
|
JavaScript
js模块系统require和import区别与联系
js模块系统require和import区别与联系
115 0
|
JavaScript 前端开发 Java
碰到Cannot find module了吗? 来看看require函数与NodeJS模块加载
目录 下面谈谈require函数 先搞清楚是什么 require能用来干什么? 非内置的模块,也想用require来加载怎么做? require函数加载原理 那么在npm registry上的库,怎么进行加载? 好了,前面提了几个围绕了是否重新开一个NodeREPL终端来require JS库的问题 解答”Cannot find module"问题 解答是否需要重启Node REPL 或者修改代码是否需要重启正在的NodeJS进程的问题 解答为何npm install lodash之后为何能够直接在node终端直接require 构建代码共享,开源文化 总结
1144 0
碰到Cannot find module了吗? 来看看require函数与NodeJS模块加载