HMR不需要深拷贝模块对象,因其目标是替换代码逻辑而非重建模块,需复用原始引用以保留state、ref、DOM关联等运行时上下文;深拷贝会切断这些引用导致状态丢失。

浅拷贝在模块热更新(HMR)中并不直接用于“复制模块”,而是被用在状态迁移、局部更新和副作用隔离等关键环节,其核心价值在于复用原始引用,避免重置可变状态,从而维持用户交互和运行时上下文的连续性。
为什么 HMR 不需要深拷贝模块对象
HMR 的目标不是重建整个模块,而是替换变更的代码逻辑,同时保留已有状态。例如:
- 一个 React 组件模块更新后,组件实例本身(含 state、ref、context)通常不销毁;新代码需“接管”旧实例。
- 一个配置对象(如
theme或formSchema)被修改时,HMR 希望只更新变化的字段,而不是丢弃整个对象并重初始化依赖它的 UI。
此时若对模块导出对象做深拷贝,反而会切断与已有 DOM、事件监听器、定时器等的关联——这些都依赖原始对象的引用身份。浅拷贝(或更准确地说:按需赋值 + 引用复用)才是符合 HMR 设计哲学的选择。
浅拷贝思想在 HMR 工具链中的体现
主流 HMR 实现(如 Vite、Webpack)虽不显式调用 Object.assign 或 copy.copy,但在运行时更新逻辑中大量采用浅拷贝语义:
-
模块导出对象的属性级合并:新模块执行后,HMR runtime 会遍历旧模块的
module.exports,将新增/变更的属性浅层覆盖到原对象上(类似Object.assign(oldExports, newExports)),保持未变更属性的引用不变。 -
React Fast Refresh 的状态保留:组件函数体被替换,但组件实例的
this.state、useState返回的 ref 对象、useRef的 current 值均不重建——它们仍指向原始内存地址,仅函数逻辑刷新。 -
Vite 的 import.meta.hot.accept 回调:开发者常在此回调中手动更新局部状态,例如:
config = { ...config, version: newVersion }——这本质是浅拷贝顶层字段,嵌套结构(如config.api.endpoints)依然复用原引用。
实际风险:嵌套可变数据仍需谨慎
浅拷贝策略能保全顶层引用,但若模块导出的是嵌套可变对象(如 { user: { profile: { name: 'Alice' } } }),而 HMR 后新模块修改了 user.profile.name,旧引用仍会受影响——这并非 HMR 的缺陷,而是开发者需遵守的约定:
- 模块导出应尽量使用不可变数据结构(如
immer管理状态)。 - 对必须修改的嵌套字段,应在 HMR 更新逻辑中主动深拷贝该子树,而非依赖全局浅拷贝。
- 避免在模块顶层导出包含定时器、WebSocket 实例等有生命周期的对象。
本质上,HMR 中的“浅拷贝”是一种设计约束:它默认信任模块状态的结构稳定性,把数据一致性责任交还给开发者,换来的是毫秒级更新和零白屏体验。

















