深入浅出Nodejs学习笔记——第二章 模块机制


待理解 阮一峰对模块的理解

待学习 模块编译、核心模块、包与npm

ES6之后有了module体系 export和import 待学习 阮一峰.ES6模块

exports

node为每一个模块提供了一个exports变量(可以说是一个对象),指向 module.exports。这相当于每个模块中都有一句这样的命令 var exports = module.exports;

module

module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。require方法用于加载模块。

验证exports = module.exports

module.exports.a = 1;
module.exports.b = '1';
exports.test = 111;
console.log('exports', exports);
console.log('module.exports', module.exports);

VS Code 控制台输出结果是

// node a.js
exports { a: 1, b: '1', test: 111 }
module.exports { a: 1, b: '1', test: 111 }

由此可见是相当于每个模块中都有一句这样的命令 var exports = module.exports;
但是exports对象的方法,但是只能对其添加属性,不能赋值到新对象

exports = {test: 1};
console.log('exports', exports);
console.log('module.exports', module.exports);

VS Code 控制台输出结果是

// node a.js
exports { test: 1 }
module.exports {}

Node.js 每个文件都是一个模块,模块内的变量都是局部变量,不会污染全局变量,在执行模块代码之前,Node.js 会使用一个如下的函数封装器将模块封装

(function(exports, require, module, __filename, __dirname) {
  // 模块的代码实际上在这里
});

__filename:当前模块文件的绝对路径
__dirname:当前模块文件据所在目录的绝对路径
module:当前的模块实例
require:加载其它模块的方法,module.require 的快捷方式
exports:导出模块接口的对象,module.exports 的快捷方式
为什么 exports 对象不支持赋值为其它对象?对上面的代码加一句话就很好理解了

const exports = module.exports;
(function(exports, require, module, __filename, __dirname) {
  // 模块的代码实际上在这里
});

其它模块 require 到的肯定是模块的 module.exports 对象,如果把 exports 对象重新赋值成新对象,就和 module.exports 对象断开了连接,自然就没用了

require

const fs = require('fs');
const express = require('express');

fs、express 都是模块名,fs 是 Node.js 内置的核心模块,express 是通过 npm 安装到 node_modules 下的第三方模块,如果出现重名,优先使用系统内置模块

单次加载 & 循环依赖

模块在第一次加载后会被缓存,如果每次调用 require('foo') 都解析到同一文件,则返回相同的对象,同时多次调用 require(foo) 不会导致模块的代码被执行多次。 Node.js 根据实际的文件名缓存模块,因此从不同层级目录引用相同模块不会重复加载。

凭借着对模块单次加载的理解对循环依赖做出简单测试

// 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:
console.log('b 开始');
module.exports.done = false;
const a = require('./a.js');
console.log('在 b 中,a.done = %s', a.done);
module.exports.done = true;
console.log('b 结束');
// node a.js
B: before logging a
{}
B: after logging a
A: before logging b
{ B: 'this is b Object' }
A: after logging b
(node:20892) Warning: Accessing non-existent property 'Symbol(nodejs.util.inspect.custom)' of module exports inside circular dependency 
(Use `node --trace-warnings ...` to show where the warning was created)
(node:20892) Warning: Accessing non-existent property 'constructor' of module exports inside circular dependency
(node:20892) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' of module exports inside circular dependency
(node:20892) Warning: Accessing non-existent property 'Symbol(Symbol.iterator)' of module exports inside circular dependency

由于我用了高版本的node会自动检测循环依赖抛出warning,忽略warning。

上面的打印输出清晰的展示出了程序运行的轨迹。在这个例子中,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修改后:
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 logging a');
console.log(a);
console.log('B: after logging a');
// node a.js
A: before logging b
{ B: 'this is b Object' }
A: after logging b
B: before logging a
{ A: 'this is a Object' }
B: after logging a
A: before logging b
{ B: 'this is b Object' }
A: after logging b
(node:20724) Warning: Accessing non-existent property 'Symbol(nodejs.util.inspect.custom)' of module exports inside circular dependency
(Use `node --trace-warnings ...` to show where the warning was created)
(node:20724) Warning: Accessing non-existent property 'constructor' of module exports inside circular dependency
(node:20724) Warning: Accessing non-existent property 'Symbol(Symbol.toStringTag)' of module exports inside circular dependency
(node:20724) Warning: Accessing non-existent property 'Symbol(Symbol.iterator)' of module exports inside circular dependency
虽然还是会报错,但是至少实现了我们想要的效果。

反思:循环依赖应该尽可能避免、减少的,我觉得出现循环依赖的情况是代码结构引起的,除非在大的生产环境中不得不去使用,不然应该尽可能的去重组代码,或者包装第三方类去统一调用

下面是第二章的思维导图,做的不完善以后待补充,建议右键新标签页打开查看清晰图片

参考文章:

  1. https://zhuanlan.zhihu.com/p/22711570
  2. https://www.yuque.com/sunluyong/node/module

声明:一个萌新|版权所有,违者必究|如未注明,均为原创|本网站采用BY-NC-SA协议进行授权

转载:转载请注明原文链接 - 深入浅出Nodejs学习笔记——第二章 模块机制


耳不闻人之非,目不视人之短,口不言人之过。