一起学习NodeJs的模块化

重新理解NodeJs的模块化

原文github地址戳这里: github.com/wangkaiwd/n…

在介绍之前,我们可以先简单了解一下javascript的模块化历程

javascript模块化

  • AMD : Asynchronous Module Definition
  • CMD : Common Module Definition
  • UMD : Universal Module Definition
  • CommonJS: Node采用该模块化方式
  • ES6模块化

推荐文章:

由于这里使用的是Nodejs,所以我们主要学习一些CMD(commonJS)的相关内容

NodeJs中的模块化

requiremodule.exports使用

NodeJs中,我们会通过module.exports或者exports来导出一个javascript文件中定义的元素,然后通过require将导出元素进行引入:

// demo02.js
console.log('1');
module.exports = () => {
  console.log('Hi, I am module demo2');
};
console.log('2');

// demo01.js
console.log('before require');
const demo2 = require('./demo02');
console.log('after require');
demo2();
复制代码

接下来我们在当前目录中打开命令行窗口,输入node demo02.js

before require
1
2
after require
Hi, I am module demo2
复制代码

所以,我们在通过require将一个模块导入的时候,不仅可以接收模块内部通过module.exports暴露的元素,还会执行相应模块内的js代码

接下来,我们在demo01.js中再加入以下代码:

const repeatDemo2 = require('./demo02');
repeatDemo2();
复制代码

执行后的输出结果如下:

before require
1
2
after require
Hi, I am module demo2
Hi, I am module demo2
复制代码

输出结果大概告诉我们这样一件事: 在首次引入某个模块的时候,NodeJs会对模块内的代码进行缓存,而当我们再次引入该模块时,会通过缓存来读取导出的内容,模块内的代码并不会重新执行。

我们可以通过require.cache属性来看到NodeJs对模块的缓存:

// 在引入模块之前和之后分别输出require.cache
// demo03.js
console.log('before require');
console.log(require.cache);
const demo2 = require('./demo02');
console.log('after require');
console.log(require.cache);
复制代码

通过截图我们可以很明显的看出,在require demo02后缓存中多了一些内容:

在阅读完上边的代码之后,这里我们可以对require的功能进行一个小结:

  1. require会引入一个模块中通过module.exports导出的元素
  2. require首次引入模块过程中,会执行模块文件中的代码,并将模块文件进行缓存
  3. 当我们再次引入该模块的时候,会从缓存中读取该模块导出的元素,而不会再次运行该文件

exportsmodule.exports

我们先看一下NodeJs官方对exports的定义:

exports变量是在模块的文件级作用域内可用的,且在模块执行之前赋值给module.expors

这句话的大概意思是说: exports并不是一个全局变量,只在模块文件内有效,并且在每个模块文件(js文件)执行之前将module.exports的值赋值给exports。即相当于在每个js文件的开头执行了如下代码:

exports = module.exports
复制代码

这意味着exportsmodule.exports指向了同一片内存空间,当为exports或者module.exports重新赋值的时候,它们将不再指向同一个引用,而我们requie引入的一直都是module.exports导出的内容

// demo04.js
// 本质上来讲:exports是module.exports的一个引用,它们指向同一片内存空间
// exports = module.exports
exports.a = 1;
module.exports = { b: 2 }; // 当引用发生变化的时候,exports不再是module.exports的快捷方式
复制代码

这时模块暴露出来的对象是{b:2}

官方也对这种行为进行了假设实现:

function require(/* ... */) {
  // 一个全局的module对象
  const module = { exports: {} };
  // 这里自执行函数传参时进行了赋值: exports = module.exports
  ((module, exports) => {
    // 模块代码在这。在这个例子中,定义了一个函数。
    function someFunc() {}
    exports = someFunc;
    // 此时,exports 不再是一个 module.exports 的快捷方式,
    // 且这个模块依然导出一个空的默认对象。
    module.exports = someFunc;
    // 此时,该模块导出 someFunc,而不是默认对象。
  })(module, module.exports);
  // 最终导出的一直都是module.exports,只不过可以通过exports来更改它们的引用,间接的改变module.exports
  return module.exports;
}
复制代码

模块之间的循环引用

假设我们有这样一种场景: 模块a.js依赖于b.js中的某个方法,而模块b.js也同样依赖于a.js中的某个方法,这样的话会不会造成死循环呢?

笔者这里写了一个demo来重现这个问题,帮助我们更好的理解模块之间的相互引用:

// demo05.js
const demo6 = require('./demo06');
console.log('I am demo5', demo6);
module.exports = { demo5: 'demo5' };

// demo06.js
const demo5 = require('./demo05');
console.log('I am demo6', demo5);
module.exports = { demo6: 'demo6' };
复制代码

执行结果如下(我们可以先猜一下):

I am demo6 {}
I am demo5 { demo6: 'demo6' }
复制代码

所以我们可以得出以下执行过程:

  1. 命令行执行node demo05.js
  2. 首先引入模块demo06.js,并且执行demo06.js,通过变量demo6来接收模块demo06.js通过module.exports导出的对象
  3. 在执行demo06.js的过程中,又引入了demo05.js,而由于demo05.js已经执行了一部分,由于缓存原因,并不会重新执行,此时demo05.js中的module.exports还是初始值{}。所以变量demo5{}
  4. demo05.js在引入demo06.js后继续执行后续代码

可以看出nodeJs对于模块之间的递归引用进行了优化,并不会引发死循环,但是需要注意的是在引入的时候要注意代码的执行顺序,否则可能会取不到对应的变量。

到这里,小伙伴在NodeJs中使用require进行引入以及通过module.exports来导出文件时的执行逻辑有了更清晰的认识呢?

转载于:https://juejin.im/post/5d075a926fb9a07edc0b56c8

猜你喜欢

转载自blog.csdn.net/weixin_34126557/article/details/93168946