龙空技术网

在Node.js中require相同模块是否会被加载多次?

程序员苏小胖 173

前言:

当前姐妹们对“node动态加载js”大概比较注意,小伙伴们都想要学习一些“node动态加载js”的相关资讯。那么小编在网摘上汇集了一些关于“node动态加载js””的相关知识,希望大家能喜欢,兄弟们快快来学习一下吧!

感谢 @晓之车 在我上一篇分享(模块化中的import与require及区别)中的提问,我也特定研究了一番,这里是一篇很早的博文,一起来看下作者的分析

Node.js也使用了CommonJS模块机制,最近在InfoQ上有一篇文章讨论了这方面的问题。这篇文章提到Node.js在载入模块时,如果之前该模块已经加载过则不会有重复开销,因为模块加载有缓存机制。这篇文章是我初审的,当时也正好在思考Jscex在Node.js使用时的模块化问题,但研究了它的规则之后,我发现在某些情况下还是可能加载多次。现在我们便来分析这个问题。

当我们使用require方法加载另一模块的时候,Node.js会去查询一系列的目录。我们可以从module.paths中得到这些路径,例如:

[ '/Users/jeffz/Projects/node-test/node_modules',  '/Users/jeffz/Projects/node_modules',  '/Users/jeffz/node_modules',  '/Users/node_modules',  '/node_modules']

这里是我在运行/User/jeffz/Projects/node-test目录下一个模块时得到的结果。可见,Node.js会从当前模块所在目录的node_modules(这里怎么不遵守Unix习惯,而使用了下划线呢?)开始找起,如果没找到再会去找上级目录的node_modules,直到根目录为止。当然,实际情况下还会有NODE_PATH环境变量标识的目录等等。当模块的位置确定之后,Node.js便会查看这个位置的模块是否已经被加载,如果已加载,则直接返回。

简单地说,Node.js是根据模块所在路径来缓存模块的。

这么看来,“相同模块是否会被加载多次”这个问题,其实就演变成了“相同模块是否会出现在不同路径里”。简单想来这似乎不太可能,因为如果我们要使用某个模块的时候,它的位置总是确定的。例如,使用npm安装的模块,总是会出现在当前目录的node_modules里,加载时总是会找到相同的路径。那么,在“间接”依赖相同模块的情况下呢?

例如我们想要使用Express框架,于是使用npm来安装,便会得到:

$ npm install expressexpress@2.5.2 ./node_modules/express ├── mkdirp@0.0.7├── qs@0.4.0├── mime@1.2.4└── connect@1.8.5

可见,Express依赖了其他一些模块,它们都存放在express模块自己的目录里面,例如./node_modules/express/node_modules/mime。好,假如我们项目自身也要使用mime项目,我们自然也可以使用npm来安装:

$ npm install mimemime@1.2.4 ./node_modules/mime 

于是我们最终得到的是这样的结构:

./node_modules├── mime└── express    └── node_modules        ├── mkdirp        ├── qs        ├── mime        └── connect

请注意,这里的mime模块便出现在两个位置上,它们名称版本都一致,完全是一个模块。那么试想,如果我们在自己的代码里加载的mime模块,以及express内部加载的mime模块是同一个吗?显然不是,可见,在这里相同的模块被重复加载了两次,产生了两个模块“实例”。

这种重复加载在一般情况下不会有太大问题,最多内存占用大一点而已,不会影响程序的正确性。但是,我们也可以轻易设想到一些意外的情况。例如,在Jscex中,每个Task对象我都会给定一个ID,不断增长。要实现这点我们需要维护一个“种子”,全局唯一。之前这个种子定义在闭包内部,但由于Jscex模块会被加载多次,这样从不同模块“实例”生成的Task对象,它们的ID便有可能重复。当然,解决这个问题也并不困难,只需要将种子定义在根对象上即可,不同的模块“实例”共享相同的根对象。

还有个问题可能就显得隐蔽些了,我们可以通过一个简单的实验来观察结果。我们先来定义一个jeffz-a模块,其中暴露出一个MyType类型:

module.exports.MyType = function () { }

然后将其发布到npm上。然后再写一个jeffz-b模块,依赖jeffz-a,并将jeffz-a中定义的MyType类型直接暴露出去:

module.exports.MyType = require("jeffz-a").MyType;

接着将jeffz-b也发布置npm上。再重新写一个测试模块,使用npm安装jeffz-a和jeffz-b,最终目录会是这样的:

./node_modules├── jeffz-a└── jeffz-b    └── node_modules        └── jeffz-a

在测试模块内,我们来测试实例与类型之间的关系:

var a = require("jeffz-a");var b = require("jeffz-b");console.log(new a.MyType() instanceof a.MyType); // trueconsole.log(new b.MyType() instanceof b.MyType); // trueconsole.log(new a.MyType() instanceof b.MyType); // falseconsole.log(new b.MyType() instanceof a.MyType); // false

从表面上看,jeffz-b和jeffz-a暴露出的应该是相同的MyType类型,它们的对象通过instanceof相互判断应该都返回true,但实际上由于jeffz-b中的jeffz-a,与我们直接加载的jeffz-a模块是不同的实例,因此MyType类型自然也不是同一个了。

这对于Jscex的影响在于,Jscex的异步模块在取消时,原本是通过判断异常对象是否为CanceledError类型来决定Task的状态为cancelled还是faulted。但由于Node.js可能会将相同的模块加载为多个实例,因此即便抛出的的确是某个实例的CancelledError,也无法通过另一个实例内部的判断。因此,目前Jscex的判断方式修改为检查异常对象的isCancellation字段,简单地解决了这个问题。

当然,Node.js这种“重复加载”的影响也并非完全是负面的,至少它天然的解决了多版本共存的问题。例如,express v2.5.2依赖mime v1.2.4,但我们程序自身又想使用mime v1.2.5。此时,express内部自然使用mime v1.2.4,而我们自己的程序使用的便是mime v1.2.5。

有些情况下您可能也想避免这种重复加载,这就必须手动地删除模块内部被间接依赖的模块,将其移动到模块查询路径的公用部分上了。就目前看来,这些操作必须手动进行,因为npm在安装模块时不会关心依赖的模块是否已经安装过了(例如在NODE_PATH环境变量标识的路径里),它一定会重新下载所有依赖的模块。可惜如果您使用的是托管形式的Node.js服务,则很有可能无法做到这一点。

原文地址:Node.js中相同模块是否会被加载多次?-CSDN博客

标签: #node动态加载js