一块儿学习NodeJs的模块化

从新理解NodeJs的模块化

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

在介绍以前,咱们能够先简单了解一下javascript的模块化历程前端

javascript模块化

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

推荐文章:java

因为这里使用的是Nodejs,因此咱们主要学习一些CMD(commonJS)的相关内容node

NodeJs中的模块化

requiremodule.exports使用

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

// 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.jsgithub

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

因此,咱们在经过require将一个模块导入的时候,不只能够接收模块内部经过module.exports暴露的元素,还会执行相应模块内的js代码缓存

接下来,咱们在demo01.js中再加入如下代码:bash

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.cache.png

在阅读完上边的代码以后,这里咱们能够对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来导出文件时的执行逻辑有了更清晰的认识呢?

相关文章
相关标签/搜索