HMR需模块系统与代码协同实现状态保持,核心是卸载前保存、加载后恢复状态;触发需满足环境启用、文件被支持、导出符合约定、未标记不可替换四条件。

HMR(Hot Module Replacement)不是自动生效的魔法,它需要模块系统(如 Webpack、Vite)和应用代码共同配合才能触发并保持状态。核心在于:模块更新时,旧模块被卸载前,需主动保存关键状态;新模块加载后,再将状态恢复,从而避免整个页面刷新或组件重渲染。
触发 HMR 的前提条件
不是所有文件变更都会触发 HMR,必须同时满足:
- 运行环境启用了 HMR 功能(例如 Webpack 中
hot: true,Vite 默认开启) - 变更的文件被对应的 HMR 插件或 loader 支持(如
vue-loader、@vitejs/plugin-react) - 模块导出符合 HMR 接口约定(常见为导出
hot.accept或使用import.meta.hot) - 模块本身未被标记为不可热替换(例如某些有副作用的全局脚本或含
window.xxx = ...的代码)
模块自身需显式声明可接受更新
Webpack 中通过 module.hot API 声明;Vite 中使用 import.meta.hot。若不调用 accept(),即使文件变了,模块也不会被替换,只会触发整页刷新(live reload)。
例如在 React 组件中:
立即学习“Java免费学习笔记(深入)”;
// Button.jsx
import { useState, useEffect } from 'react';
export default function Button() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('mounted');
return () => console.log('unmounted');
}, []);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// ✅ 启用 HMR:告诉系统“我支持局部更新”
if (import.meta.hot) {
import.meta.hot.accept();
}
这段代码让 Vite 知道该模块可以被安全替换。但注意:仅调用 accept() 不等于状态保留——它只控制是否替换模块,不处理状态迁移。
状态保持靠开发者手动实现
HMR 不会自动保存组件实例、闭包变量或 DOM 状态。要维持 UI 行为连续性,需在模块卸载前读取状态,并在新模块激活后还原。
典型做法是利用 HMR 的生命周期钩子:
-
hot.dispose(callback)(Webpack)或import.meta.hot.dispose(callback)(Vite):模块即将被替换前执行,用于保存状态 -
hot.accept(callback)或import.meta.hot.accept(callback):新模块就绪后执行,用于恢复状态
例如保存和恢复一个计数器值:
let savedCount = 0;
if (import.meta.hot) {
// 卸载前保存
import.meta.hot.dispose((data) => {
data.count = count; // 假设 count 是当前状态
});
// 更新后恢复
import.meta.hot.accept((newModule) => {
if (newModule.default) {
// 可结合 React Fast Refresh 或手动 patch 状态
// 实际项目中更推荐用框架封装方案(如 @vitejs/plugin-react 自动处理)
}
});
}
注意:纯函数组件没有实例,React 生态通常依赖 React Fast Refresh 自动保持 state 和 ref;而 Vue 3 的 setup() 组件由 vue-loader 或 Vite 的 @vitejs/plugin-vue 在编译期注入状态保持逻辑。
为什么有时 HMR 失效或状态丢失?
常见原因包括:
- 模块导出了非默认的、无法被 HMR 插件识别的结构(如多个命名导出且无默认导出)
- 在模块顶层写了不可撤销的副作用(如
document.body.append(...)、setInterval未清理) - 使用了不兼容 HMR 的第三方库(未提供
hot.dispose支持或内部强依赖模块引用) - 开启了严格模式但未正确处理模块卸载(例如事件监听器未移除,导致重复绑定)
排查建议:检查浏览器控制台是否有 [HMR] Updated modules 日志;确认模块是否真正进入 dispose 阶段;使用 import.meta.hot?.data 查看跨更新的数据传递是否正常。










