HMR 和全局刷新的区别是什么?在 React / Vue 中是如何实现组件级别 HMR 的?
在 Webpack 开发环境中,HMR(Hot Module Replacement,热模块替换) 和 全局刷新(Full Reload / Live Reloading) 是两种截然不同的模块更新机制。
以下是它们的深度区别,以及 React 和 Vue 实现组件级 HMR 的底层原理。
一、 HMR 与 全局刷新的区别
| 维度 | HMR (热模块替换) | 全局刷新 (Full Reload) |
|---|---|---|
| 刷新行为 | 局部更新:只替换修改的模块,不刷新浏览器页面 | 整页刷新:相当于手动按下 F5 或 Cmd+R |
| 应用状态 | 保留状态:如输入框内容、弹窗开启状态、Pinia/Redux 中的数据依然存在 | 丢失状态:所有 DOM 重新渲染,JS 重新执行,状态彻底清空 |
| 更新速度 | 极快(毫秒级):只重新编译和注入变更的模块文件 | 较慢:需要重新加载页面所有资源、重新解析 JS/CSS |
| 用户体验 | 无缝衔接,无页面闪烁,极大提升开发效率 | 页面明显闪烁,开发复杂表单/多步骤交互时体验极差 |
| 底层实现 | 基于 WebSocket 通信 + Webpack HMR API (module.hot.accept) |
基于 WebSocket 通信 + window.location.reload() |
二、 基础原理:Webpack 的 HMR 接口
无论是 React 还是 Vue,底层都依赖 Webpack 提供的原生的 module.hot API。
当某个模块变更时,Webpack 会通过 WebSocket 向上层抛出更新通知。如果是原生 JS,我们需要这样写才能实现热更新:
if (module.hot) {
module.hot.accept('./component.js', () => {
// 1. 获取更新后的模块
const nextComponent = require('./component.js');
// 2. 手动替换 DOM 或重新执行逻辑
render(nextComponent);
});
}
但手动写 module.hot.accept 非常繁琐,因此 React 和 Vue 通过各自的 Loader / Plugin 将这套逻辑自动化了。
三、 React 如何实现组件级 HMR?
React 目前标准的 HMR 方案是 React Fast Refresh(取代了早期易出错的 React Hot Loader)。
它的实现主要依赖三个部分:
- Babel 插件 (
react-refresh/babel):编译期代码改造。 - 运行时 (
react-refresh/runtime):运行期的状态追踪与组件替换。 - Webpack 插件 (
@pmmmwh/react-refresh-webpack-plugin):连接 Webpack HMR API 与 Fast Refresh 运行时。
具体实现步骤:
编译期注入(Babel 标记):
Babel 插件会分析文件,为每一个 React 组件(函数组件/类组件)注入隐式的签名(Signature)和注册代码。- 它会记录 Hooks 的使用顺序和类型。
- 将组件注册到全局注册表:
$RefreshReg$(MyComponent, "MyComponent")。
捕获更新(Webpack HMR):
当改变一个组件时,Webpack 触发 HMR,拉取最新的模块代码。状态保留与重新渲染(React Fiber 机制):
- 安全检查:Fast Refresh 会对比新旧组件的 Hooks 签名。如果 Hook 的数量或顺序没变(例如只是修改了 JSX 或普通的逻辑),系统判定为“安全”。
- Fiber 节点更新:React 利用自身的 Fiber 架构,将新组件的
type(函数定义)替换掉旧 Fiber 节点上的type,但保留该 Fiber 节点上的memoizedState(即useState/useReducer的数据)。 - 局部重绘:触发 React 重新渲染该组件及其子节点。
注意:如果修改了 Hooks 的顺序/数量,或者导出的是非 React 组件(如纯工具函数),Fast Refresh 会判定为“不安全”,退回到全局刷新。
四、 Vue 如何实现组件级 HMR?
Vue 的 HMR 实现主要依赖于 vue-loader(针对 Vue 2/3 单文件组件 .vue)和 vue-hot-reload-api。
由于 Vue 的单文件组件(SFC)天然拆分为 <template>、<script> 和 <style>,Vue 的 HMR 可以做到极精细的分级处理:
1. <style> 修改(样式热重载)
- 原理:
style-loader或vue-style-loader会拦截 CSS 修改。 - 过程:直接通过 JS 替换
<head>中对应<style>标签的内容,完全不影响任何 JS 逻辑和组件状态。
2. <template> 修改(模板热重载)
- 原理:Vue 编译期会将
<template>编译为render函数。 - 过程:
- 当仅修改模板时,
vue-loader只会重新编译render函数。 - 利用 Webpack HMR API 获取最新的
render函数。 - 直接替换旧组件实例上的
render方法,并调用组件实例的$forceUpdate()或触发响应式更新。 - 结果:DOM 局部更新,组件的
data/setup中的状态完全保留。
- 当仅修改模板时,
3. <script> 修改(脚本热重载)
- 原理:修改 JS 逻辑(如
methods、setup内部逻辑)改变了组件的定义。 - 过程:
- Vue 认为组件的行为已经发生了根本改变。
- 通过
vue-hot-reload-api,销毁旧的组件实例(触发unmount/destroyed钩子)。 - 在原 DOM 位置重新挂载(Reload)新的组件实例。
- 结果:该组件的局部状态会重置,但父组件的状态、全局状态(Pinia/Vuex)依然保留,页面不会闪烁。
总结
- HMR vs 全局刷新:HMR 是“局部无缝替换 + 保留状态”,全局刷新是“摧毁重建 + 丢失状态”。
- React 实现核心:依靠 Fast Refresh,结合 Babel 注入签名和 React Fiber 架构,在替换组件函数的同时强行保留 Fiber 节点上的 Hook 状态。
- Vue 实现核心:依靠
vue-loader对 SFC 结构进行细分对待。模板修改精细替换 render 函数保留状态;脚本修改销毁重建局部组件;样式修改直接替换 CSS。