模块执行上下文是hmr实施“外科手术式”更新的基础环境,它包含词法作用域、导出对象、闭包变量等;hmr不销毁该上下文,而是通过动态代理导出、干预模块缓存、显式清理与恢复机制,在其中精准替换模块代码并维持状态连贯性。

模块热替换(HMR)不是靠“重执行整个模块上下文”来实现的,而是在已有执行上下文中动态卸载旧模块、注入新模块并维持状态连贯性。它的关键不在重建上下文,而在精准接管与局部更新。
模块执行上下文在HMR中起什么作用?
模块执行上下文(Module Execution Context),即模块被 require 或 import 后实际运行时所处的环境——包括其词法作用域、导出对象(module.exports / export)、闭包变量、定时器、事件监听器等。HMR 的设计前提是:不销毁这个上下文,而是在它内部做“外科手术式”更新。
例如:
- 一个 React 组件模块导出了一个函数组件,它内部持有一个
useState的闭包状态; - 修改该组件的 JSX 后保存,HMR 不会销毁当前组件实例,而是让新的组件函数替代旧函数,同时保留 DOM 节点和 React Fiber 树中的状态。
这就要求 HMR 运行时必须:
- 识别模块是否支持热更新(通过
module.hot.accept()显式声明); - 在替换前调用
module.hot.dispose()清理副作用(如清除定时器、解绑事件); - 替换后触发
module.hot.accept()回调,执行局部刷新逻辑(如强制重渲染、更新样式、重置缓存)。
HMR 如何在不重置上下文的前提下完成模块替换?
模块导出对象被动态代理
Webpack 的 HMR Runtime 会把每个模块的exports对象包装为可更新的引用。当新模块加载后,旧exports的引用会被悄悄指向新模块的导出内容,而外部已持有的引用(比如父模块里import { fn } from './a'中的fn)仍有效——只要fn本身没被覆盖或重定义。-
模块缓存(
require.cache)被主动干预
Node.js 和浏览器端 HMR 都会劫持模块缓存机制:- 浏览器端:Webpack 将模块打包进 bundle,
__webpack_require__函数维护内部模块缓存表; - 当
module.hot.accept()触发时,HMR Runtime 主动从缓存中删除旧模块条目,并用新模块代码重新执行、生成新exports,再写入缓存; - 父模块再次
require或import时,拿到的就是新版本。
- 浏览器端:Webpack 将模块打包进 bundle,
-
上下文状态靠开发者手动桥接
HMR 本身不自动保存组件 state、Redux store、Canvas 上下文等运行时数据;它只保证模块代码可换。真正“保状态”,依赖框架层适配(如 React Fast Refresh、Vue Loader 的<script setup></script>响应式绑定)或开发者显式编写accept回调:if (module.hot) { module.hot.dispose(() => { // 清理旧 canvas 动画帧 cancelAnimationFrame(animId); }); module.hot.accept(() => { // 用新 draw 函数重启动画,但复用同一 canvas 元素 animId = requestAnimationFrame(draw); }); }
为什么不能直接“重执行模块上下文”?
因为重执行会带来不可控副作用:
- 全局变量重复声明(如
let count = 0再次执行 → 重置); - 事件监听器重复绑定(导致多次触发);
- 定时器叠加(多个
setInterval并行); - DOM 节点重复插入或丢失;
- React/Vue 实例被销毁重建,状态全丢。
所以 HMR 的设计哲学是:最小侵入、显式控制、状态自治——模块自己决定哪些要清、哪些要留、哪些要重算。
本质上,HMR 把“模块执行上下文”当作一个可插拔的单元,而不是一次性的执行快照。











