Quiet
  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我

bajiu

  • 主页
  • 归档
  • 分类
  • 标签
  • 链接
  • 关于我
Quiet主题
  • NodeJs
  • JavaScript

bajiu
前端

2021-06-08 21:23:02

一.require()时发生了什么?

Node.js 中, 模块加载过程分为 5 步:

  1. 路径解析 (Resolution) :根据模块标识找出对应模块(入口)文件的绝对路径
  2. 加载 (Loading): 如果是 JSON 或 JS 文件, 就把文件内容读入内存. 如果是内置的原生模块, 将其共享库动态链接到当前 Node.js 进程
  3. 包装 (Wrapping): 将文件内容(JS 代码)包进一个函数, 建立模块作用域, exports, require, module 等作为参数注入
  4. 执行 (Evaluation): 传入参数,执行包装得到的函数
  5. 缓存 (Caching): 函数执行完毕后, 将 module 缓存起来,并把 module.exports 作为 require() 的返回值返回

其中, 模块标识 (Module Identifiers) 就是传入require(id) 的第一个字符串参数id, 例如 require('./myModule') 中的 './myModule', 无需指定后缀名(但带上也无碍)

对于.、..、/开头的文件路径, 尝试当做文件、目录来匹配, 具体过程如下:

  1. 路径存在并且是个文件,就当做 JS 代码来加载 (无论文件后缀名是什么, require(./myModule.abcd)完全正确)
  2. 若不存在,依次尝试拼上.js、.json、.node(Node.js 支持的二进制扩展)后缀名
  3. 如果路径存在并且是个文件夹,就在该目录下找 package.json, 取其 main 字段,并加载指定的模块(相当于一次重定向)
  4. 如果没有package.json, 就依次尝试index.js、index.json、index.node

对于模块标识不是文件路径的, 先看是不是 Node.js 原生模块(fs、path等. 如果不是, 就从当前目录开始, 逐级向上在各个node_modules下找, 一直找到顶层的/node_modules, 以及一些全局目录

  • NODE_PATH 环境变量中指定的位置
  • 默认的全局目录: $HOME/.node_modules、$HOME/.node_libraries 和 $PREFIX/lib/node

找到模块文件后,读取内容,并包一层函数:

(function(exports, require, module, __filename, __dirname) {
// Module code actually lives in here
});

执行时从外部注入这些模块变量 (exports, require, module, __filename, __dirname) ,模块导出的东西通过 module.exports 带出来,并将整个 module 对象缓存起来,最后返回require()结果

循环依赖

模块之间可能会出现循环依赖, 对此 Node.js 的处理策略非常简单:

// module1.js
exports.a = 1;
require('./module2');
exports.b = 2;
exports.c = 3;

// module2.js
const module1 = require('./module1');
console.log('module1 is partially loaded here', module1);

module1.js 执行中引用了 module2.js, module2 又引了 module1, 此时 module1 尚未加载完 (exports.b = 2; exports.c = 3;还没执行) 而在 Node.js 里,只加载了一部分的模块也可以正常引用

所以 module1 执行的顺序是:

module1 is partially loaded here { a: 1 }

Node.js 内部是怎么实现的

实现上,模块加载的绝大多数工作都是由 module 模块来完成的:

const Module = require('module');
console.log(Module);

Module 是个函数/类:

function Module(id = '', parent) {
  this.id = id;
  this.path = path.dirname(id);
  // 即module.exports
  this.exports = {};
  this.parent = parent;
  updateChildren(parent, this, false);
  this.filename = null;
  this.loaded = false;
  this.children = [];
}

每加载一个模块都创建一个 Module 实例, 模块文件执行完后, 该实例仍然保留, 模块导出的东西依附于 Module 实例存在

模块加载的所有工作都是由 module 原生模块来完成的, 包括Module._load、Module.prototype._compile

Module._load

Module._load() 负责加载新模块、管理缓存,具体如下:

Module._load = function(request, parent, isMain) {
  // 0.解析模块路径
  const filename = Module._resolveFilename(request, parent, isMain);
  // 1.优先找缓存 Module._cache
  const cachedModule = Module._cache[filename];
  // 2.尝试匹配原生模块
  const mod = loadNativeModule(filename, request, experimentalModules);
  // 3.未命中缓存,也没匹配到原生模块,就创建一个新的 Module 实例
  const module = new Module(filename, parent);
  // 4.把新实例缓存起来
  Module._cache[filename] = module;
  // 5.加载模块
  module.load(filename);
  // 6.如果加载/执行出错了,就删掉缓存
  if (threw) {
    delete Module._cache[filename];
  }
  // 7.返回 module.exports
  return module.exports;
};

Module.prototype.load = function(filename) {
  // 0.判定模块类型
  const extension = findLongestRegisteredExtension(filename);
  // 1.按类型加载模块内容
  Module._extensions[extension](this, filename);
};

支持的类型有.js、.json、.node3 种:

// Native extension for .js
Module._extensions['.js'] = function(module, filename) {
  // 1.读取JS文件内容
  const content = fs.readFileSync(filename, 'utf8');
  // 2.包装、执行
  module._compile(content, filename);
};

// Native extension for .json
Module._extensions['.json'] = function(module, filename) {
  // 1.读取JSON文件内容
  const content = fs.readFileSync(filename, 'utf8');
  // 2.直接JSON.parse()完事
  module.exports = JSONParse(stripBOM(content));
};

// Native extension for .node
Module._extensions['.node'] = function(module, filename) {
  // 动态加载共享库
  return process.dlopen(module, path.toNamespacedPath(filename));
};

Module.prototype._compile

Module.prototype._compile = function(content, filename) {
  // 1.包一层函数
  const compiledWrapper = wrapSafe(filename, content, this);
  // 2.把要注入的参数准备好
  const dirname = path.dirname(filename);
  const require = makeRequireFunction(this, redirects);
  const exports = this.exports;
  const thisValue = exports;
  const module = this;
  // 3.注入参数、执行
  compiledWrapper.call(thisValue, exports, require, module, filename, dirname);
};

清掉缓存

默认 Node.js 模块加载过就有缓存,而有些时候可能想要禁掉缓存, 强制重新加载一个模块, 比如想要读取能被用户频繁修改的 JS 文件 (如webpack.config.js)

此时可以手动删掉挂在 require.cache 身上的 module.exports 缓存:

delete require.cache[require.resolve('./b.js')]

然而,如果 b.js 还引用了其它外部 (非原生) 模块,也需要一并删除:

const mod = require.cache[require.resolve('./b.js')];
// 把引用树上所有模块缓存全都删掉
(function traverse(mod) {
  mod.children.forEach((child) => {
    traverse(child);
  });

  console.log('decache ' + mod.id);
  delete require.cache[mod.id];
}(mod));
上一篇

Image.onload垃圾回收

下一篇

前端模块化之ESModule

©2024 By bajiu.