闭包是支撑多版本共存热更新的关键结构基础,通过封装模块状态与行为实现版本隔离、避免状态污染,并需配合dispose/accept生命周期管理及模块缓存清理。

闭包本身不是热更新机制,但它能成为支撑“多版本共存”热更新模块的关键结构基础——关键在于用闭包封装模块状态与行为,配合明确的版本隔离、引用清理和动态加载逻辑,避免新旧版本副作用交叉干扰。
闭包提供天然的版本作用域隔离
每个热更后的业务模块实例,都应由一个独立函数执行并返回其功能接口,该函数内部形成闭包,捕获当前版本的私有状态(如配置、缓存、定时器ID等)。这样不同版本的模块彼此不共享变量,不会因热替换导致状态污染。
- 每次热更新都应调用全新函数生成新闭包,而非复用旧函数体
- 避免在闭包外暴露可变状态对象;若需跨版本通信,应通过显式传参或中心事件总线,而非共享闭包变量
- 例如:const createService = (version) => { const cache = new Map(); return { getVersion: () => version, getData: (key) => cache.get(key) } } —— 每次调用 createService 都生成独立闭包,version 和 cache 互不干扰
必须配对管理闭包生命周期:dispose + accept
热更新不是简单替换函数,而是旧闭包退出、新闭包入场的过程。Webpack HMR 要求在被更新模块内声明 module.hot.dispose 清理上一版闭包持有的资源,再用 module.hot.accept 初始化新版闭包。
- dispose 中需清除闭包内启动的定时器、事件监听、WebSocket 连接、全局副作用注册等
- accept 中重新执行模块初始化逻辑,创建新闭包,并绑定新资源
- 不能把 dispose/accept 逻辑抽到工具函数里跨模块调用,否则 Webpack 无法关联到当前模块的更新生命周期
多版本共存 ≠ 多版本同时运行,而是平滑过渡
所谓“共存”,是指旧版本尚未完全卸载(比如异步任务还在跑)、新版本已就绪待接管的状态。这需要设计可中断、可重入的业务逻辑,而非强依赖单例或全局状态。
- 对外暴露的 API 应具备“版本路由”能力,例如通过 context.version 或请求头决定走哪个闭包实例
- 长期运行的任务(如轮询)应在 dispose 中主动终止,并在 accept 中按需重启,避免多个版本重复发起相同请求
- 若业务要求灰度期间新旧逻辑并行,可让主入口根据策略分发到不同闭包实例,但每个实例仍保持自身闭包封闭性
注意闭包与模块缓存的耦合风险
Node.js 或某些嵌入式环境(如 Lua、ILRuntime)中,模块缓存(require.cache / AppDomain 程序集映射)若未正确清理,会导致旧闭包持续被引用,即使你新建了函数也无法真正替换——旧闭包里的变量依然活着。
- 在 Node.js 热更中,需手动删除 require.cache 中对应路径条目,并清空父模块 children 引用
- 在 ILRuntime 中,AppDomain 卸载前需确保无跨域引用残留,否则闭包捕获的对象可能引发内存泄露
- 闭包本身不解决模块缓存问题,但它让“清理旧状态”这件事变得结构清晰:所有要清理的东西,都应定义在闭包内部或其明确依赖链上










