[NodeJs系列]NodeJs模块机制

news/2024/6/20 17:39:33 标签: json, 前端, c/c++

注: 1. 本文涉及的nodejs源码如无特别说明则全部基于v10.14.1

如果你对NodeJs系列感兴趣,欢迎关注前端神盾局或笔者微信(w979436427)交流讨论node学习心得

Nodejs 中对模块的实现

本节主要基于NodeJs源码,对其模块的实现做一个简要的概述,如有错漏,望诸君不吝指正。

当我们使用require引入一个模块的时候,概况起来经历了两个步骤:路径分析和模块载入

路径分析

路径分析其实就是模块查找的过程,由_resolveFilename函数实现。

我们通过一个例子,展开说明:

const http = require('http');
const moduleA = requie('./parent/moduleA');
复制代码

这个例子中,我们引入两种不同类型的模块:核心模块-http和自定义模块moduleA

对于核心模块而言,_resolveFilename会跳过查找步骤,直接返回,交给下一步处理

if (NativeModule.nonInternalExists(request)) {
    // 这里的request 就是模块名称 'http'
    return request;
}
复制代码

而对于自定义模块而言,存在以下几种情况(_findPath)

  1. 文件模块
  2. 目录模块
  3. 从node_modules目录加载
  4. 全局目录加载

这些在官方文档中已经阐述的很清楚了,这里就不再赘述。

如果模块存在,那么_resolveFilename会返回该模块的绝对路径,比如/Users/xxx/Desktop/practice/node/module/parent/moduleA.js

载入模块

获取到模块地址后,Node就开始着手载入模块。

首先,Node会查看模块是否存在缓存中:

// filename 即模块绝对路径
var cachedModule = Module._cache[filename];
if (cachedModule) {
    updateChildren(parent, cachedModule, true);
    return cachedModule.exports;
}
复制代码

存在则返回对应缓存内容,不存在则进一步判断该模块是否是核心模块:

if (NativeModule.nonInternalExists(filename)) {
    return NativeModule.require(filename);
}
复制代码

如果模块既不存在于缓存中也非核心模块,那么Node会实例化一个全新的模块对象


function Module(id, parent){
  // 通常是模块绝对路径
  this.id = id;
  // 要导出的内容
  this.exports = {};
  // 父级模块
  this.parent = parent;
  this.filename = null;
  // 是否已经加载成功
  this.loaded = false;
  // 子模块
  this.children = [];
}

var module = new Module(filename, parent);
复制代码

而后Node会根据路径尝试载入。

function tryModuleLoad(module, filename) {
  var threw = true;
  try {
    module.load(filename);
    threw = false;
  } finally {
    if (threw) {
      delete Module._cache[filename];
    }
  }
}
复制代码

对于不同的文件扩展名,其载入方法也有所不同。

  • .js文件(_compile)

通过fs同步读取文件内容后将其包裹在指定函数中:

Module.wrapper = [
  '(function (exports, require, module, __filename, __dirname) { ',
  '\n});'
];
复制代码

调用执行此函数:

compiledWrapper.call(this.exports, this.exports, require, this,
                                  filename, dirname);
复制代码

通过fs同步读取文件内容后,用JSON.parse解析并返回内容

var content = fs.readFileSync(filename, 'utf8');
try {
    module.exports = JSON.parse(stripBOM(content));
} catch (err) {
    err.message = filename + ': ' + err.message;
    throw err;
}
复制代码
  • .node

这是用C/C++编写的扩展文件,通过dlopen()方法加载最后编译生成的文件。

return process.dlopen(module, path.toNamespacedPath(filename));
复制代码
  • .mjs

这是用于处理ES6模块的扩展文件,是NodeJs在v8.5.0后新增的特性。对于这类扩展名的文件,只能使用ES6模块语法import引入,否则将会报错(启用--experimental-modules的情况下)

throw new ERR_REQUIRE_ESM(filename);
复制代码

如果一切顺利,就会返回附加在exports对象上的内容

return module.exports;
复制代码

模块循环依赖

接下来我们来探究一下模块循环依赖的问题:模块1依赖模块2,模块2依赖模块1,会发生什么?

这里只探究commonjs的情况

为此,我们创建了两个文件,module-a.js和module-b.js,并让他们相互引用:

module-a.js

console.log(' 开始加载 A 模块');
exports.a = 2;
require('./module-b.js');
exports.b = 3;
console.log('A 模块加载完毕');
复制代码

module-b.js

console.log(' 开始加载 B 模块');
let moduleA = require('./module-a.js');
console.log(moduleA.a,moduleA.b)
console.log('B 模块加载完毕');
复制代码

运行module-a.js,可以看到控制台输出:

开始加载 A 模块
开始加载 B 模块
2 undefined
B 模块加载完毕
A 模块加载完毕
复制代码

这时因为每个require都是同步执行的,在module-a完全加载前需要先加载./module-b,此时对于module-a而言,其exports对象上只附加了属性a,属性b是在./module-b加载完成后才赋值的。

QA

  1. 如何删除模块缓存?

可以通过delete require.cache(moduleId)来删除对应模块的缓存,其中moduleId表示的是模块的绝对路径,一般的,如果我们需要对某些模块进行热更新,可以使用此特性,举个例子:

// hot-reload.js
console.log('this is hot reload module');

// index.js
const path = require('path');
const fs = require('fs');
const hotReloadId = path.join(__dirname,'./hot-reload.js');
const watcher = fs.watch(hotReloadId);
watcher.on('change',(eventType,filename)=>{
    if(eventType === 'change'){
        delete require.cache[hotReloadId];
        require(hotReloadId);
    }
});
复制代码
  1. Node中可以使用ES6 模块吗?

从8.5.0版本开始,NodeJs开始支持原生ES6模块,启用该功能需要两个条件:

  1. 所有使用ES6模块的文件扩展名都必须是.mjs
  2. 命令行选项--experimental-modules node --experimental-modules index.mjs
node --experimental-modules index.mjs
复制代码

但是截止到NodeJs v10.15.0,ES6模块的支持依旧是实验性的,笔者并不推荐在公司项目中使用

参考

  1. nodejs-loader.js
  2. 朴灵. 深入浅出Node.js


http://www.niftyadmin.cn/n/1693258.html

相关文章

【SVM】推算公式的由来

主要说明SVM是干什么用的,怎么来的,如何推导公式。 (其中有些公式没有严格打出来,比如矩阵W的转置,大家意会即可)。 一、SVM怎么来的 1、介绍PLA PLA是perceptron learning algorithm,主要处理线性可分的数据。 比…

Input系统之键值映射

一. 概述 android系统的输入事件来源在linux内核提供的/dev/input的设备节点下, 当该设备下及诶点有数据刻度时,将数据独处并进行一系列的翻译和加工,然后在所有的窗口中寻找合适的接受者,并派发给它; 输入系统总体流程如下(引之深入理解android卷3 ): 1.1 开发环境 系统: ubu…

【CNN】经典模型总结及Resnet理解

之前一直都是看,没有自己完整总结一遍,现在做一个简单的总结。 并主要针对Resnet做一个介绍。 主要参考于: http://blog.csdn.net/app_12062011/article/details/62886113 https://www.zhihu.com/question/38499534 一、Lenet 第一个CNN…

【MLDL】logistics regression理解

以前有学过linear classification、linear regression和logistics regression,这次做一下总结,并主要推导一下交叉熵损失函数的由来和梯度下降法。 一、概述 开头先祭出林轩田老师讲义中的一张图 PLA、Linear Regression到logistics regression的区别。…

sql 中常见的控制流语句

控制流语句:1 begin .....end 2 if ...else 例如:if exists (select * from 表名称 ) begin selct * from 表名称 end 3 while break countinue while语句用于设置重复执行的sql语句或者语句块continue语句可以让语句跳过contunue 语句之后的语句回到…

【CACHE disabled】win7开机提示缓存状态为disabled

前景:某天早上我照常开机,开始一天的学习,结果以往20秒的开机时间变成了一分多钟,我感觉到很诡异,于是我就点了360修复(哎,不该点的)。 后来我就发现,电脑也变得超级卡&…

RHCE学习笔记3

单元6 管理物理存储 IRedHat 6分区后必须重启。 分区,一般只能(最多)4个主分区,sda5是扩展分区,扩展分区不能直接使用,需要创建逻辑分区。/dev/sda表示第一块硬盘,/dev/sdb表示第二块硬盘&#…

开了500多家店,茵曼还是不懂新零售?

电商红利正盛之时,涌现了很多淘宝明星,茵曼便是其中一个。与茵曼同时期的淘品牌还有韩都衣舍、裂帛等等。曾几何时,淘品牌炙手可热。茵曼、韩都衣舍、裂帛都曾分别向见证会递交招股书,意图争夺“淘品牌第一股”。 如今&#xff0c…