a.js和b.js互相require怎么执行? —— CommonJS模块探究
学习Node.js遇到2个模块互相引用的问题,有点搞不清楚。我觉得这个模块机制对于学习Node.js来说,搞清代码执行顺序来说,十分有必要。
所以在读了相关资料,并实践之后,愉快的在这分享一下成果。
CommonJS中模块的"循环引用"问题
先说结论: CommonJS中的做法时,一旦某个模块被”循环引用“,也就是这个模块没有加载完,就进入了循环,所以原则是,只exports已经执行的那部分,没执行的不输出。
如果没看懂,没关系。我第一遍也没明白。但是看下这个例子就好理解啦~
a.js
module.exports.done = false
var b = require('./a.js')
console.log(`在a模块中,b.done=${b.done}`)
module.exports.done = true
console.log('a模块执行完毕')
b.js
module.exports.done = false
var b = require('./a.js')
console.log(`在a模块中,b.done=${b.done}`)
module.exports.done = true
console.log('a模块执行完毕')
在终端执行
node a.js
下面我们梳理代码的执行过程。然后自己运行代码验证一下就好啦~
- 1.a模块第1行导出done = false;
- 2.第2行require('./b.js'),于是a模块暂停,进入并执行b模块
- 3.b模块第1行导出done = false,之后require('./a.js'),此时出现"死循环",关键是CommonJS会如何处理呢? 请看上面的结论,这里的a模块还没执行完,仅仅执行了一句module.exports.done = false,所以就导出的就是这个。
- 4.之后b模块执行完后,跳回到a模块第3行,然后依次打印。
终端的结果
所以问题的关键,就是弄清楚,当a模块还没执行完成时,b模块里require a模块的话,执会导出a模块已经执行完成的部分。
上面的例子中,a模块只执行了
module.exports.done = false
所以就导出了这个。
那么建议同学们,试一下把require('./b.js')放到第一行,试试看执行结果会有什么不同?
如果有同学实践后,还是不理解的话,可以看看参考资料里阮一峰老师的文章,或者看一眼我这篇文章的第二个部分,也许缺少一些CommonJS的其他知识。
CommonJS中需要知道的一些知识点
- 知识点1:require('./a.js')时,不仅仅是引用方法,如果是第一个require的话,就直接执行a.js了
- 知识点2:再次require('./a.js')并不会有反复执行a.js的BUG,而是直接从缓存中取值。
被缓存的模块,可以通过require.cache里看到
- 知识点3:module.exports和exports的区别
// 简单说,require只认module.exports,
// 而初始化时 exports是module.exports的引用
exports = module.exports
- 知识点4:CommonJS里发生"循环引用"的话,就exports已经执行的部分。ES6发生"循环引用"的话,每次import只生成一个模块引用,不影响使用变量
关于CommonJS里"循环引用"的解决方法
+ 办法1:module.exports = () => {const moduleDemo = require('./b.js')} 避免require时直接执行,使其延迟执行。
+ 办法2:把module.exports提到第一行,但不是万能的,因为要导出的东西可能依赖文件下面的计算
CommonJS与ES6模块中的"循环引用"的区别
每一个模块化方案,都可能会遇到"循环引用"的情况,但是执行方式不一样。
因为AMD和CMD规范将来会被淘汰,所以ES6和CommonJS更值得拿出来研究一下。
ES6的引入命令是import,例如
import {foo} from './some.js'
ES6引入foo时,不会执行./some.js,而是生成一个该模块的引用,当需要用foo变量时,在到some模块里取值。
所以ES6是动态引用,不会缓存some.js,而是生成一个引用对象。
在import时不去加载。自然地,所谓"循环引用"也就不是问题了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。