require 加载过程:缓存与路径解析
本节目标
- 理解
require的「路径解析」规则(相对/绝对/node_modules) - 理解「模块缓存」机制与它的双刃剑
- 会用
require.cache做热重载
require 加载分两步:解析路径 → 执行模块 → 缓存结果。
// 运行环境:Node.js 16+(保存为 .cjs)
// 1) 路径解析优先级:
// require('./a') 相对路径,找 a.js / a/index.js / a.json
// require('lodash') 非相对 → 从当前目录向上找 node_modules/lodash
// require('/abs/a') 绝对路径
// 2) 缓存:同一模块同进程只执行一次
// counter.cjs
let n = 0
module.exports = { tick() { return ++n } }
// main.cjs
const a = require('./counter.cjs')
const b = require('./counter.cjs') // 不会重新执行,b 和 a 是同一份
console.log(a.tick(), b.tick()) // 1 2(共享同一个 n)
// 3) 强制重新加载(热重载技巧):删掉缓存再 require
delete require.cache[require.resolve('./counter.cjs')]
const c = require('./counter.cjs') // 这次重新执行,n 归零
console.log(c.tick()) // 1双刃剑:缓存让重复 require 很快,但也意味着「模块顶层只跑一次」——如果你的模块有副作用(连数据库、起定时器),它会持久存在。
名词解释
模块缓存(Module Cache):Node 把「已加载模块的导出」存在 require.cache 里,同进程内再次 require 同一路径直接返回缓存,不重新执行文件。它像「备忘录」:第一次算完记下来,以后直接抄答案。好处是性能,代价是「模块顶层的副作用只发生一次」。
路径解析(Resolution):require 把「你写的路径」翻译成「磁盘上的真实文件」。相对路径(./)从当前文件找; bare 名(lodash)从 node_modules 向上找;绝对路径直接定位。理解解析顺序,才能解释「为什么 require 一个名字就能找到装在 node_modules 的包」。
课后练习
练习 1:为什么「改了被 require 的模块文件,程序却没生效」?
答案:因为
require.cache里还存着旧的导出,Node 不会重新读文件。开发时要「重启进程」或删require.cache对应项才能加载新代码。这也是为什么 Node 开发离不开nodemon之类热重启工具。
练习 2:require('lodash') 为什么不用写 ./ 或完整路径?
答案:没有
.//..///前缀的 bare 标识符,Node 走「node_modules 解析」:从当前目录逐级向上找node_modules/lodash,找到包入口(package.json 的 main/exports)。这是 npm 包能被「按名引入」的机制。
总结
缓存是 Node 模块系统里最容易被忽视、却影响最大的机制。我的观点:理解缓存,你才真正理解「Node 模块为什么是单例」。很多诡异 bug(配置改了不生效、单例状态串了)根因都在缓存。我的实践:写「纯函数模块」时把状态留在调用方,别塞模块顶层,这样缓存带来的单例性不会咬你;写「有副作用的模块」(连接、定时器)要清楚它只初始化一次。热重载/测试时,delete require.cache[require.resolve(x)] 是你的标准逃生阀。一句话:缓存让快,也让你必须想清「模块顶层只跑一次」意味着什么。