Vite 的 HMR 到底是怎么知道我改了哪个模块的?

萌新.福萍 阅读 12

我在用 Vite 开发 React 项目时,发现只要改了某个组件,浏览器就只更新那个组件,不会整个页面刷新。但我不太明白它是怎么精准定位到具体模块的?

我试过在控制台看网络请求,发现会收到类似 {"type":"update","updates":[{"type":"js-update","path":"/src/components/Counter.jsx"}]} 的消息,但没搞懂 Vite 是怎么在文件改动后生成这个路径并通知客户端的?

是不是跟 import 的依赖图有关?有没有相关的源码逻辑可以参考?

我来解答 赞 1 收藏
二维码
手机扫码查看
1 条解答
银银的笔记
Vite 的 HMR 能精准定位到具体模块,核心就靠两件事:一个是开发服务器启动时构建出的「模块依赖图」,另一个是文件改动后触发的「依赖追踪 + 精准更新路径计算」。

开发阶段 Vite 会用 esbuild 把每个模块(比如 .jsx.ts)单独编译成 ESM 模块,同时记录每个模块的依赖关系——比如 A.jsx 引了 B.jsx,那依赖图里就会有个边 A → B。这个依赖图存在内存里,不是靠 Webpack 那种静态分析打包出来的完整 bundle,而是按需实时返回的单个模块。

当你改了某个文件,比如 Counter.jsx,Vite 的 chokidar 监听到文件变动,就会走一套叫 transformRequest + moduleGraph 的逻辑:

- 先重新请求这个模块(触发一次编译,生成新代码和新的 module id)
- 然后根据模块图回溯它的「依赖链」:谁依赖了它?谁又依赖了谁?(用的是深度优先遍历)
- 如果这个模块是「HMR 边界模块」(比如它自己没被其他模块依赖,或者它的父级是 HMR boundary),那就直接更新它;否则就得往上找最近的 boundary

关键来了:Vite 的 HMR boundary 默认是入口模块或被动态 import() 的模块,但 React 项目里一般用 @vitejs/plugin-react,它内部加了个 react-refresh 插件,会自动识别出 export default function Counter() 这种组件,把它标记成可热更新的模块,然后生成一段 HMR runtime 代码注入到浏览器端。

浏览器收到 {"type":"update","updates":[...]} 这类消息时,会调用 import.meta.hot.accept 注册的回调,比如 React Refresh 的 runtime 会根据模块路径找到对应组件实例,只替换它的实现逻辑,不刷新整个页面。

源码上你可以重点关注这几个文件(Vite 2.x/3.x):
- packages/vite/src/node/server/hmr.ts:处理 HMR 消息和模块更新逻辑
- packages/vite/src/node/server/transformRequest.ts:模块请求和缓存失效
- packages/plugin-react/src/plugin.ts:注入 react-refresh 的 HMR 逻辑

说白了,Vite 不是靠猜测,而是靠精确的模块图 + ESM 动态引用 + runtime 注入的 accept 回调,才能做到「改哪更新哪」。这玩意儿比 Webpack 的 HotModuleReplacementPlugin 轻量多了,也快多了。
点赞 2
2026-02-25 16:15