它是怎么做到“极速 HMR”的。
🚀 1. 为什么 Vite 的 HMR 能这么快?
Vite 的核心思想:
🧠 不需要重新构建 bundle,而是定向更新实际变动文件
Webpack:
- 文件改了 → Webpack 重新构建部分 bundle → 发送补丁 → 替换模块
Vite:
- 文件改了 → 浏览器重新请求这个文件的 ESM 版本
- 如果有依赖关系 → 只更新依赖链,而不是打包
所以 Vite HMR 快速的根本原因:天然 ESM,无需 bundle,无需 diff,无需 patch。
📦 2. HMR 核心模块结构
目录(大致)
vite/
├─ server/
│ ├─ index.ts → 创建 dev server(核心)
│ ├─ ws.ts → WebSocket 通信
│ ├─ hmr.ts → HMR 的依赖跟踪 & 更新逻辑
│ ├─ moduleGraph.ts → 模块依赖图(最关键)
│ ├─ plugins/
│ ├─...
│ ├─ transformRequest.ts → 单文件的编译/转换
│ └─...核心组件:
- ModuleGraph 模块图(追踪模块依赖)
- Watcher 文件监听器(chokidar)
- WS WebSocket 系统(向浏览器推送更新事件)
- 热更新处理器:server.handleHMRUpdate()
- 客户端 HMR 运行时(/vite/client)
🕸 3. ModuleGraph —— HMR 的大脑
Vite 为每个模块维护了一个图结构:
ts
class ModuleNode {
url: string
importedModules: Set<ModuleNode>
importers: Set<ModuleNode>
}例子:
A.js → import B.js
B.js → import C.jsModuleGraph 会记录:
- B 的 importers =
- C 的 importers =
➡️ 当 C 改了 → 需要通知 B ➡️ 如果 B 也没有 HMR 接收器 → 再通知 A(向上冒泡)
📡 4. 文件变化后(HMR 整条链路)
下面进入最关键部分:HMR 是怎么真的动起来的?
✨ 第 1 步:监听文件变化(chokidar)
在 server/index.ts 中:
ts
watcher.on('change', (file) => {
server.handleHMRUpdate(file)
})当你编辑某个文件,比如 src/App.vue:
File changed: src/App.vue✨ 第 2 步:handleHMRUpdate() 处理更新
重点函数:server.handleHMRUpdate(file)
它会:
- 找到对应的 ModuleNode
- 执行该模块的插件
hmr钩子(例如 Vue 插件) - 采集依赖链中接受更新的模块
- 通过 WebSocket 发送更新事件
✨ 第 3 步:确定哪些模块“接受更新”
HMR 是有两种处理方式:
✔ 有 import.meta.hot.accept()
→ 局部热更新(只刷新相关模块)
❌ 没 HMR 处理器
→ 根据依赖反向查找 “importers” → 找到最近的 HMR 边界 → 如果找不到 → 全量刷新页面
例如:
A.vue → import B.js → import C.js如果改了 C.js:
C.js 有没有 accept? ❌ 没有
它的 importer B.js 有没有 accept? ❌ 没有
再向上看 A.vue ✔ 有 Vue 的 HMR accept
➡️ 最终更新 A.vue(触发 Vue 局部更新)
✨ 第 4 步:WebSocket 推送更新消息
代码来自 ws.ts:
ts
ws.send({
type: 'update',
updates: [{ path, timestamp }]
})Vite 使用 ws://localhost:5173 建立一个 WebSocket,它只发 JSON:
json
{
"type": "update",
"updates": [
{
"path": "/src/components/MyComp.vue",
"timestamp": 1679999999999
}
]
}✨ 第 5 步:浏览器端(客户端 HMR runtime)
HMR 客户端运行时在:
vite/client主要逻辑:
- 接收 WebSocket push
- 根据
path动态调用import(path + "?t=timestamp") - 替换模块
- 执行
import.meta.hot.accept()回调
示例(简化):
js
socket.onmessage = async (msg) => {
if (msg.type === 'update') {
for (const update of msg.updates) {
import(update.path + '?t=' + update.timestamp)
}
}
}非常直接:重新 import 文件而已!
没有 patch,没有 diff,没有 bundle —— 所以快。
✨ 第 6 步:如果没有模块接受更新 → 整页刷新
js
location.reload()这就是为什么有时候 HMR 失败,会自动刷新页面。
🧩 5. 为什么 Vite 这么快?(总结)
因为:
- 使用浏览器原生 ESM (No Bundle)
- 改哪个文件就更新哪个
- HMR 是基于依赖图的精准更新
- 客户端仅需要重新 import 新模块
- 无需 patch, 无需 rebuild, 无需 Pre-bundle
这就是 Vite 能秒级热更新的根本原因。