node 如何实现 CommonJS
require、module、exports 是什么?
console.log(module);
console.log(require);
console.log(exports);
console.log(module.exports === exports);打印结果
Module {
    id: ...,
    path: ...,
    exports: ...,
    filename: ...,
    ...
}
[Function: require] {
    ...
}
{}
true- module 是一个 Module 对象,记录当前模块信息, 
- require 是一个方法,用来导入模块 
- exports 是一个对象,包含当前模块导出的内容 
- module 对象的 exports 属性指向 exports 对象 
为什么可以直接使用 require、module、exports?
因为 node 会执行代码之前会先运行包装函数,来对代码块进行首尾包装,包装函数类似于:
function wrap(script) {
  return `(function (exports, require, module) { 
        ${script}
    })`;
}模拟包装函数执行
const moduleIndexFunction = wrap(`
    const util = require('./util.js');
    const sum = util.getSum(1, 2);
`);
const moduleUtilFunction = wrap(`
    const getSum = (a, b) => a + b;
    exports.getSum = getSum;
`);得到
const moduleIndexFunction = `(function (exports, require, module) {
  const util = require('./util.js');
  const sum = util.getSum(1, 2);
})`;
const moduleUtilFunction = `(function (exports, require, module) {
  const getSum = (a, b) => a + b;
  exports.getSum = getSum;
})`;在模块加载的时候,会传入require、module、exports等参数,模拟效果如下:
// util.js
(function (exports, require, module) {
  const getSum = (a, b) => a + b;
  exports.getSum = getSum;
})(exports, require, module);模块标识符
require 导入模块的情况有 3 种
const fs = require('fs'); // ① node 核心模块
const sayName = require('./hello.js'); // ② 文件模块
const crypto = require('crypto-js'); // ③ 第三方模块require 接受唯一参数作为找到模块的标识符
核心模块
像fs、http、path等标识符,会被作为 nodejs 的核心模块
文件模块
- ./和- ../作为相对路径的文件模块
- /作为绝对路径的文件模块
- require 会将路径转换成真实路径来找到模块 
第三方模块
- 非路径形式也非核心模块的模块,将作为自定义模块 
- require 从当前目录 node_modules 开始,依次向上递归在每一级目录的 node_modules 中查找,直至根目录的 node_modules 
- 找到模块目录后,会查找 - package.json的 main 属性指向的文件,如果未找到 package.json 或 main 属性,会依次查- index.js、- index.json、- index.node
模块执行顺序
CommonJS 模块会同步加载并执行,遵循深度优先遍历
// a.js
require('./c');
console.log('我是 a 文件');
// b.js
console.log('我是 b 文件');
// c.js
console.log('我是 c 文件');
// index.js
require('./a');
require('./b');
console.log('node 入口文件');打印结果
我是 c 文件
我是 a 文件
我是 b 文件
node 入口文件require 函数的模拟实现
// 用于缓存已加载过的模块
const cache = {};
// id 为路径标识符
function require(id) {
  // 从缓存中查找该模块
  const cachedModule = cache[id];
  // 若已加载则直接取走缓存的 exports 对象
  if (cachedModule) {
    return cachedModule.exports;
  }
  // 创建当前模块的 Module 对象
  const module = {
    id,
    exports: {},
    loaded: false, // 用于表示当前模块是否被加载
    //...
  };
  // 将当前模块存入缓存
  cache[id] = module;
  // 对模块进行包装后运行
  (function (exports, require, module) {
    // 模块中的代码
  })(module.exports, require, module);
  module.loaded = true; // 加载完成
  // 返回 exports 对象
  return module.exports;
}require 如何避免重复加载
首次加载的模块的 Module 对象会被缓存,下次 require 时会从缓存中直接获取 Module 对象的 exports 对象
require 如何避免循环引用
// a.js
const bExports = require('./b');
console.log('我是 a 文件');
exports.fa = function () {
  console.log('执行 fa');
};
// b.js
const aExports = require('./a');
console.log('我是 b 文件');
module.exports = { say: 'Hello World!' };
// index.js
const aExports = require('./a');
const bExports = require('./b');
console.log('node 入口文件');node index.js 执行步骤
- 执行 - require('./a')
- 创建 a 的 Module 对象,将 module.exports 存入缓存 
- 执行 a.js 中的代码 
- 执行 - require('./b')
- 创建 b 的 Module 对象,将 module.exports 存入缓存 
- 执行 b.js 中的代码 
- 执行 - require('./a'),从缓存中取出模块 a 的 module.exports 对象赋给- aExports
- 执行 - console.log('我是 b 文件');
- 让 b 模块的 module.exports 指向一个新对象 - { say: 'Hello World!' }
- 回到 a.js,将 b 的 module.exports 对象赋给 - bExports
- 执行 - console.log('我是 a 文件');
- 给 a 的 module.exports 对象添加一个方法 - fa
- 回到 index.js,将 a 的 module.exports 对象赋给 - aExports
- 执行 - require('./b'),从缓存中取出 b 的 module.exports 对象赋给- bExports
- 执行 - console.log('node 入口文件');
最终打印结果
我是 b 文件
我是 a 文件
node 入口文件需要注意的是,步骤 6 执行时,模块 a 的 exports 对象中还未添加 fa 方法,如果此时调用 fa 会报错
require 通过缓存来避免重复执行代码,从而避免循环引用
require 动态加载
require 本质上是一个函数,因此可以在任意地方调用,即在任意上下文动态加载模块
// a.js
exports.fa = () => {
  const b = require('./b');
};
// b.js
module.exports = { b: '123' };
// index.js
const { fa } = require('./a');
fa();如上,在 fa 调用时动态加载模块 b
使用 exports 导出内容
// a.js
exports.fa = () => {
  console.log('执行 fa');
};
// b.js
const { fa } = require('./a');
fa();可以通过在 exports 对象上添加属性来导出内容
exports 的本质
在模块被首位包装后执行时,exports 作为一个参数被传入模块,此时它指向 module.exports。所以在 exports 上添加属性,相当于给 module.exports 添加属性,在 require 执行完以后,module.exports 对象会被返回
为什么不能给 exports 赋值来导出内容
exports = { a: '123' };如果直接将一个对象赋给 exports,会导致 exports 指向一个新对象,而 require 返回的是 module.exports,和 exports 指向的新对象无关
module.exports 导出内容
require 最终返回的是 module.exports,因此在模块中可以给 module.exports 赋值来导出内容
let a = 1;
module.exports = a; // 导出函数
module.exports = [1, 2, 3]; // 导出数组
module.exports = function () {}; //导出方法