跳到主要内容

CommonJS 与循环依赖

CommonJS 的加载机制是什么?​

  • 场景: 主要用于 Node.js, 本地文件加载远快于 ESM;
  • 同步: 构建、实例化与求值一次性同步完成;
  • 遍历: 深度优先后序;
  • 缓存: 在查找下一个模块之前先执行当前模块代码 (直到 require 语句) 并实时缓存;
  • 缓存命中: 再次 require 同一模块时直接读取缓存, 不再执行;

导出变量为什么是值拷贝?​

  • 机制: 导入方拿到的是导出变量在导入时刻的副本;
  • 结果: 导出方之后修改变量, 导入方读到的仍是旧值;
  • 对比: ESM 的 live binding 指向同一内存地址, 见 ESM 加载机制;

循环依赖时输出顺序如何推演?​

// index.js
var a = require("./a");
console.log("入口模块引用a模块: ", a);

// a.js
exports.a = "原始值-a模块内变量";
var b = require("./b");
console.log("a模块引用b模块: ", b);
exports.a = "修改值-a模块内变量";

// b.js
exports.b = "原始值-b模块内变量";
var a = require("./a");
console.log("b模块引用a模块", a);
exports.b = "修改值-b模块内变量";
  • 输出:
b模块引用a模块 { a: '原始值-a模块内变量' }
a模块引用b模块: { b: '修改值-b模块内变量' }
入口模块引用a模块: { a: '修改值-a模块内变量' }
  • 推演:
    1. 执行 index.js 并缓存, 加载 a.js 并缓存;
    2. a.js 先把 原始值-a模块内变量 写入 exports.a, 再加载 b.js;
    3. b.js 缓存自身后 require("./a"), 命中的是已缓存的 a.js, 此时 a 仍是原始值, 因此是值拷贝的旧值;
    4. b.js 把 b 改写为修改值后返回 a.js, a.js 读到修改值;
    5. a.js 把 a 改写为修改值后返回入口, 入口读到修改值;