添加链接
link之家
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接
相关文章推荐
风流倜傥的台灯  ·  PySpark UDF - 知乎·  1 年前    · 
Node.js中的模块循环依赖及其解决

Node.js 开发一般不容易遇到真正的模块循环依赖的情况,可是当你的项目开始达到一定的复杂度之后,你很有可能在你的 Node.js 编码生涯中遇到几次。而且如果你之前没有关于这方面的意识,Debug 可能会花费不少的时间。

我在最近的项目中就遇到了这种情况,而且不能轻易通过项目架构的重构来解决。具体来说,A 文件中需要用 B 文件中某些函数,B 文件又需要用到 A 文件中的某些函数。

实际上,Node.js 官网上就有 关于模块循环 require() 的说明

在官网给出的例子中,有 3 个模块:main.js、a.js、b.js。其中 main.js 有对 a.js 和 b.js的引用,而 a.js 和 b.js 又是相互引用的关系(详细情况请参阅上段末的超链接)。

官网上点出了这种模块循环的情况,并且解释清楚了原因(但并没有给出具体可行的解决方案):

When main.js loads a.js, then a.js in turn loads b.js. At that point, b.js tries to load a.js. In order to prevent an infinite loop, an unfinished copy of the a.js exports object is returned to the b.js module. b.js then finishes loading, and its exports object is provided to the a.js module.

简单说就是,为了防止模块载入的死循环,Node.js 在模块第一次载入后会把它的结果进行缓存,下一次再对它进行载入的时候会直接从缓存中取出结果。所以在这种循环依赖情形下,不会有死循环,但是却会因为缓存造成模块没有按照我们预想的那样被导出(export,详细的案例分析见下文)。

官网给出了三个模块还不是循环依赖最简单的情形。实际上,两个模块就可以很清楚的表达出这种情况。根据递归的思想,解决了最简单的情形,这一类任意大小规模的问题也就解决了一半(另一半还需要探明随着问题规模增长,问题的解将会如何变化)。

下面是一个两个模块循环依赖的问题最简情形:

A.js:

let b = require('./B');
console.log('A: before logging b');
console.log(b);
console.log('A: after logging b');
module.exports = {
    A: 'this is a Object'

B.js:

let a = require('./A');
console.log('B: before logging a');
console.log(a);
console.log('B: after logging a');
module.exports = {
    B: 'this is b Object'

运行 A.js,将会看到如下输出:

B: before logging a
B: after logging a
A: before logging b
{ B: 'this is b Object' }
A: after logging b

JavaScript 作为一门解释型的语言,上面的打印输出清晰的展示出了程序运行的轨迹。在这个例子中,A.js 首先 require 了 B.js, 程序进入 B.js,在 B.js 中第一行又 require 了 A.js。

如前文所述,为了避免无限循环的模块依赖,在 Node.js 运行 A.js 之后,它就被缓存了,但需要注意的是,此时缓存的仅仅是一个未完工的 A.js(an unfinished copy of the a.js)。所以在 B.jsrequire A.js 时,得到的仅仅是缓存中一个未完工的 A.js,具体来说,它并没有明确被导出的具体内容(A.js 尾端)。所以 B.js 中输出的 a 是一个空对象。

之后,B.js 顺利执行完,回到 A.js 的 require 语句之后,继续执行完成。

想要解决这个问题有一个很简明的方法,那就是在循环依赖的每个模块中先导出自身,然后再导入其他模块(对于本文的举例来说,实际只需改动 A.js 就可以达到效果)。

话不多说,放码过来:

A.js:

module.exports = {
    A: 'this is a Object'
let b = require('./B');
console.log('A: before log b');
console.log(b);
console.log('A: after log b');

B.js:

module.exports = {
    B: 'this is b Object'
let a = require('./A');
console.log('B: before log a');
console.log(a);
console.log('B: after log a');

此时,在 A 和 B 中,都在 require 之前就导出了自身需要导出的模块,此时输出则是这样:

B: before log a
{ A: 'this is a Object' }
B: after log a
A: before log b
{ B: 'this is b Object' }
A: after log b

可以看到 B 中按我们的预期输出了 A 中导出的值。

这种解决办法可行的原因也很简单,还是因为 JavaScript 是一门解释型的语言,在 require 其他模块之前,已经把自身需要导出的部分都导出了,所以即便有模块载入缓存,也不影响最终结果按预期进行。

这种办法几乎没什么副作用,唯一稍令强迫症感到不快就是这种顺序与我们通常的书写顺序不符。一般我们都会先把 require 写在源文件开头,exports 放到后面的位置。唯一需要祈祷的是,之后接手项目的代码猴儿不会因为觉得这个顺序看着碍眼又把它改回去。鉴于此点,在导入导出语句上添加合理的解释性注释变得很重要

其他相关问题

实际上,我还自己实验并查阅了一些资料来探索是否有其他的解决办法,但那些办法要么是适用于特定的情形和设计模式之下,要么就没有上述方法简洁,本文就不赘述了。如果有兴趣,可以参看本文末尾的 References 链接。如果你发现有更好的解决办法,欢迎在评论区留言。

要想彻底弄明白 Node.js 模块加载的相关问题,一定得去读读 Node.js 相关部分的源码。其次,推荐阅读《深入浅出 Node.js》第二章与阮一峰的这篇日志

有趣的是,ES6 特性中已经有了更优秀的 import/export 模块加载机制,就不会存在这样的问题(原因参考 References 第5条),然而 Node.js 还并不支持。Github 上有人提出过这个问题,Node.js 基金会成员 @bnoordhuis 对此的回复是:

In a nutshell, require() is not going anywhere - removing it would break too much for too little gain - but we’ll almost certainly end up supporting ES6 import/export somehow, details TBD.

Support for ES6 modules first needs to land in V8.

详细的讨论可以到这里查看。

虽然因为 V8 的原因 Node.js 官方还不能支持 import/export,不过我们依然可以借助 Babel 来提前在 Node.js 使用这个特性,感兴趣的同学可以参考这里

References

  1. Modules | Node.js Documentation
  2. Circular dependencies in node.js
  3. node.js的循环依赖 - cnode
  4. Node.js 中的循环依赖 - sf
  5. JavaScript 模块的循环加载 - 阮一峰
  6. nodejs中模块循环依赖的解决方案 #65
如果你想第一时间查看我最新的文章,欢迎RSS订阅我的个人博客:http://maples7.com。知乎专栏将延期数天到数月不等不完全同步博客中的文章。微信公众号:Chapters_Of_Maples7,只更新自己随手写的想到的只言片语或图片。本文内容可能已经不是最新,查看原文:Node.js中的模块循环依赖及其解决Node.js 开发一般不容易遇到真正的模块循环依赖的情况,可是当你的项目开始达到一定的复杂度之后,你很有可能在你的 Node.js 编码生涯中遇到几次。而且如果你之前没有关于这方面的 一个简单的纯typescript+nodejs项目,在一次编译运行代码时,遇到一个奇怪的报错: TypeError: Class extends value undefined is not a constructor or null 调试堆栈信息,发现是一个类继承的父类是空。在多次尝试调试,考虑场景,查找资料的情况下,确实论证为nodejs循环引用。但是我觉得我的使用场景是很正常的场景,觉得nodejs在这块的处理确实有点奇怪。 classA: import log from './
文章目录什么是循环依赖CommonJS循环依赖的处理ESM的处理Webpack对循环依赖的处理参考 什么是循环依赖 循环依赖一般会伴随模块化一起出现,就是在a模块依赖b模块,而b模块又依赖a模块。 在以前开发时就遇到过这种情况,在store初始化时使用了utils模块的方法,而utils有利用到了store的数据 在开发时没有问题,但是在打包后运行时就报错了,实际上就是遇到了循环依赖的问题。 对于循环依赖,Node默认的CommonJS模块和ES6的模块以及Webpack打包时的处理各不相同 循环依赖就是相互依赖,实际项目难免会遇到这种情况,(a.js 依赖 b.js , b.js 依赖 a.js) 尤其是现在都是模块化的工程,引用想用一个js文件必须先引入。 举个例子: 左边是三个电阻(a.js),右边是弹出的旋钮模态框可设置电阻值(b.js) 因为有多个电阻,但只有一个模态框,所以弹出时要记录当前是哪个电阻,当前电阻的值 (a --> b ) 当模态框...
const functionB = require ( 'b.js' ) ; function logicA ( ) { fuinctionB ( ) ; // undefined functionB // b.js const functionA = require ( 'a.js' ) ; function logicB ( ) { functionA ( ) ; // undefined functionA 因此,在此回购,我将演示如何使用DI(依赖项注入)设计模式来解决循环依赖项的问题。
Module._load = function(request, parent, isMain) { if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); } var filename = Module._resolveFilename(request, parent, isMa
有一次,跟新别人的代码且npm install后,启动时报错:Cyclic dependency; 最简单粗暴的解决方法就是,删除node_modules文件夹和package-lock.json文件。重新npm install,然后npm start 或npm run dev,正常启动。
Node.js面试题是指在面试过程可能会被问到的与Node.js相关的问题。这些问题涉及到Node.js的基本概念、核心模块、事件驱动编程、异步编程、模块开发、错误处理、性能优化等方面。面试官通过询问这些问题来评估面试者对Node.js的理解和应用能力。 以下是一些可能会出现的Node.js面试题示例: 1. 请解释Node.js的特点和优势。 2. 什么是事件驱动编程,Node.js的事件驱动编程是如何实现的? 3. 请解释Node.js的非阻塞I/O模型,并说明其在高并发场景下的优势。 4. 如何在Node.js处理异步操作?请列举几种常见的异步编程方式。 5. Node.js的事件循环是什么?请解释事件循环的执行过程。 6. 请解释Node.js模块开发,并说明模块开发的优势。 7. 如何创建一个HTTP服务器并监听端口?请给出示例代码。 8. Node.js的Buffer是什么?请解释Buffer的作用和使用场景。 9. 如何在Node.js管理包依赖?请解释npm的作用和常见用法。 10. 如何在Node.js执行子进程?请给出一个实例代码。 以上是一些常见的Node.js面试题示例,面试者可以根据自己的实际经验和学习情况准备答案。同时,建议面试者在准备面试时还要了解Node.js的常见性能优化策略、错误处理机制等相关知识,以便在面试给出更全面的回答。
Solved “openssl config failed: error:02001003:system library:fopen:No such process” for Nodejs Windo 12952