乐趣区

关于node.js: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 办法
  2. 通过 Module._resolveFilename 获取模块的门路 fileName
const filename = Module._resolveFilename(request, parent, isMain);
  1. 依据 fileName 判断是否存在该模块的缓存

    • 如果存在缓存,则调用 updateChildren 办法在更新缓存内容,并返回缓存
    • 如果不存在缓存,则继续执行
  2. 当做原生模块,调用 loadNativeModule 办法进行加载

    • 如果加载胜利,则返回该原生模块
    • 否则,继续执行
  3. 依据以后模块名(门路)和父模块对象生成一个 Module 实例:
const module = cachedModule || new Module(filename, parent);
  1. 再判断该模块是否是入口文件
if (isMain) {
    process.mainModule = module;
    module.id = '.';
}
  1. 将该模块的实例存入到 Module 的缓存中
Module._cache[filename] = module;

  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办法的返回值

具体取得上图后果的代码是:

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 查找模块的流程如下:

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

~ 本文完~

学习乏味的常识,结识乏味的敌人,塑造乏味的灵魂!

大家好!我是〖编程三昧〗的作者 隐逸王,我的公众号是『编程三昧』,欢送关注,心愿大家多多指教!

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

退出移动版