Vite 的 HMR 到底是怎么知道我改了哪个模块的?
我在用 Vite 开发 React 项目时,发现只要改了某个组件,浏览器就只更新那个组件,不会整个页面刷新。但我不太明白它是怎么精准定位到具体模块的?
我试过在控制台看网络请求,发现会收到类似 {"type":"update","updates":[{"type":"js-update","path":"/src/components/Counter.jsx"}]} 的消息,但没搞懂 Vite 是怎么在文件改动后生成这个路径并通知客户端的?
是不是跟 import 的依赖图有关?有没有相关的源码逻辑可以参考?
开发阶段 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 轻量多了,也快多了。