基于本文回答

播面 播面

文解图解播客视频,多维讲透八股文
0
评论

HMR 和全局刷新的区别是什么?在 React / Vue 中是如何实现组件级别 HMR 的?

在 Webpack 开发环境中,HMR(Hot Module Replacement,热模块替换)全局刷新(Full Reload / Live Reloading) 是两种截然不同的模块更新机制。

以下是它们的深度区别,以及 React 和 Vue 实现组件级 HMR 的底层原理。


一、 HMR 与 全局刷新的区别

维度 HMR (热模块替换) 全局刷新 (Full Reload)
刷新行为 局部更新:只替换修改的模块,不刷新浏览器页面 整页刷新:相当于手动按下 F5Cmd+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,我们需要这样写才能实现热更新:

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)。

它的实现主要依赖三个部分:

  1. Babel 插件 (react-refresh/babel):编译期代码改造。
  2. 运行时 (react-refresh/runtime):运行期的状态追踪与组件替换。
  3. Webpack 插件 (@pmmmwh/react-refresh-webpack-plugin):连接 Webpack HMR API 与 Fast Refresh 运行时。

具体实现步骤:

  1. 编译期注入(Babel 标记):
    Babel 插件会分析文件,为每一个 React 组件(函数组件/类组件)注入隐式的签名(Signature)注册代码

    • 它会记录 Hooks 的使用顺序和类型。
    • 将组件注册到全局注册表:$RefreshReg$(MyComponent, "MyComponent")
  2. 捕获更新(Webpack HMR):
    当改变一个组件时,Webpack 触发 HMR,拉取最新的模块代码。

  3. 状态保留与重新渲染(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-loadervue-style-loader 会拦截 CSS 修改。
  • 过程:直接通过 JS 替换 <head> 中对应 <style> 标签的内容,完全不影响任何 JS 逻辑和组件状态

2. <template> 修改(模板热重载)

  • 原理:Vue 编译期会将 <template> 编译为 render 函数。
  • 过程
    1. 当仅修改模板时,vue-loader 只会重新编译 render 函数。
    2. 利用 Webpack HMR API 获取最新的 render 函数。
    3. 直接替换旧组件实例上的 render 方法,并调用组件实例的 $forceUpdate() 或触发响应式更新。
    4. 结果:DOM 局部更新,组件的 data / setup 中的状态完全保留

3. <script> 修改(脚本热重载)

  • 原理:修改 JS 逻辑(如 methodssetup 内部逻辑)改变了组件的定义。
  • 过程
    1. Vue 认为组件的行为已经发生了根本改变。
    2. 通过 vue-hot-reload-api,销毁旧的组件实例(触发 unmount / destroyed 钩子)。
    3. 在原 DOM 位置重新挂载(Reload)新的组件实例。
    4. 结果:该组件的局部状态会重置,但父组件的状态、全局状态(Pinia/Vuex)依然保留,页面不会闪烁。

总结

  • HMR vs 全局刷新:HMR 是“局部无缝替换 + 保留状态”,全局刷新是“摧毁重建 + 丢失状态”。
  • React 实现核心:依靠 Fast Refresh,结合 Babel 注入签名和 React Fiber 架构,在替换组件函数的同时强行保留 Fiber 节点上的 Hook 状态。
  • Vue 实现核心:依靠 vue-loader 对 SFC 结构进行细分对待。模板修改精细替换 render 函数保留状态;脚本修改销毁重建局部组件;样式修改直接替换 CSS