彻底搞懂 Node.js 中的 Require 机制(源码分析到手写实践)

本文深入探讨 Node.js 中的 Require 机制,从 CommonJS 原理到源码分析,包括加载原生模块、普通文件模块和 C++ 扩展文件模块的过程。详细阐述 require 加载的八大步骤,解析模块缓存、路径分析、加载逻辑,并解答了关于 require 是否同步、exports 与 module.exports 的区别等问题。此外,还涉及了 CommonJS 与 ES6 Module 的差异以及 Node.js 的 vm 模块。
摘要由CSDN通过智能技术生成

本文你能学到什么

  1. 自己手写实现一个 require,面试用也可以。

  2. 如何看 Node.js 源码

  3. require 函数是如何产生的?为什么在 module 中可以直接使用。

  4. require 加载原生模块时候如何处理的,为什么 require('net') 可以直接找到

  5. Node.jsrequire 会出现循环引用问题吗?

  6. require 是同步还是异步的?为什么?

  7. exportsmodule.exports 的区别是什么?

  8. 你知道 require 加载的过程中使用了 vm 模块吗?vm 模块是做什么的?vm 模块除了 require 源码用到还有哪些应用场景。

请注意我上面提出的问题,本文学完后看看是否都搞能懂。最好跟着练习一遍手写 require 源码,美滋滋。

什么是 CommonJS

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

模块分类

  • 原生(核心)模块:Node 提供的模块我们都称之为原生模块

    • 内建模块:Node.js 原生提供的模块中,由纯 C/C++ 编写的称为内建模块

    • 全局模块:Node.js在启动时,会生成一个全局量 process

    • 除了上面两种可以直接 require 的所有原生模块

  • 文件模块:用户编写的模块

    • 普通文件模块:node_modules 下面的模块,或者我们自己开发时候写的每个文件内容。

    • C++ 扩展模块:用户自己编写的 C++ 扩展模块或者第三方 C++ 扩展模块

模块加载

介绍了上面的模块分类,正常应该到介绍不同模块的加载环节,这里不这样,只列出目录。先带你看一遍源码,再手写一下,然后我想你自己总结一下这几种模块的加载区别。

加载 Node.js 原生模块

本文不包括直接调用内建纯C/C++模块,也不推荐这样使用,因为我们正常调用的原生模块都是通过 js封装一层,它们自己再去调用,你想直接调用的 Node.js提供的存C/C++ 内建模块,js 封装的一层也都能做到。那部分内容放在 Node.jsC++ 那些事的文章中介绍。

require 加载普通文件模块

require 加载 C++ 扩展文件模块

require 加载原理(源码分析与手写)

require 源码解析图

画了一个源码导图,可以直接跟着导图学一遍配上文章讲解,效果更佳。(导图太大了上传不清晰,需要找我要吧。)

require 源码并不复杂,这里采用的是边看源码边手写的方式讲解(我们最终实现的require 是简易版本,一些源码提到,但是简易版本不会实现),实现 require 其实就是实现整个 Node.js 的模块加载机制,Node.js 的模块加载机制总结下来一共八步。

网上一些文章只分成了3-4步,我这里做了一下细化,为了彻底搞懂我开篇提到的一些问题。

1. 基础准备阶段

Node.js 模块加载的主流程都在 Module 类中,在源码的https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L150 中进行了基础 Module 类定义,这个构造函数中的内容主要做一些值的初始化,我们自己对照着实现下,为了和源码有一个区别,本文使用 KoalaModule 命名。

function KoalaModule(id = '') {
  this.id = id;       // 这个id其实就是我们require的路径
  this.path = path.dirname(id);     // path是Node.js内置模块,用它来获取传入参数对应的文件夹路径
  this.exports = {};        // 导出的东西放这里,初始化为空对象
  this.filename = null;     // 模块对应的文件名
  this.loaded = false;      // loaded用来标识当前模块是否已经加载
}

KoalaModule._cache = Object.create(null); //创建一个空的缓存对象
KoalaModule._extensions = Object.create(null); // 创建一个空的扩展点名类型函数对象(后面会知道用来做什么)

然后在源码中你会找到 require 函数,在 KoalaModule 的原型链上,我们实现下

Module.prototype.require = function(id) {
    return Module._load(id, this, /* isMain */ false);
};

在源码中你会发现又调用了_load函数,找到源码中的 _load 函数,(源码位置:https://github.com/nodejs/node/blob/master/lib/internal/modules/cjs/loader.js#L724)下面的所有步骤都是在这个函数中完成调用和 return的,实现简易版_load函数。

KoalaModule._load = function (request) {    // request是我们传入的路劲参数
  // 2.路径分析并定位到文件
  const filename = KoalaModule._resolveFilename(request);

  // 3.判断模块是否加载过(缓存判断)
  const cachedModule = koalaModule._cache[filename];
  if (cachedModule !== undefined) {
    return cachedModule.exports;
  }
  // 4. 去加载 node 原生模块中
  /*const mod = loadNativeModule(filename, request);
   if (mod && mod.canBeRequiredByUsers) return mod.exports;*/
   
 
  • 4
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值