Vite HMR源码级讲解
我会还原 Vite 源码模块结构、文件流转、WebSocket 通信、模块依赖图、热更新链路 等,让你能完全理解它是怎么做到“极速 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 → 单文件的编译/转换
│ └─...
1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
核心组件:
- ModuleGraph 模块图(追踪模块依赖)
- Watcher 文件监听器(chokidar)
- WS WebSocket 系统(向浏览器推送更新事件)
- 热更新处理器:server.handleHMRUpdate()
- 客户端 HMR 运行时(/vite/client)
# 🕸 3. ModuleGraph —— HMR 的大脑
Vite 为每个模块维护了一个图结构:
class ModuleNode {
url: string
importedModules: Set<ModuleNode>
importers: Set<ModuleNode>
}
1
2
3
4
5
2
3
4
5
例子:
A.js → import B.js
B.js → import C.js
1
2
2
ModuleGraph 会记录:
- B 的 importers = { A }
- C 的 importers = { B }
➡️ 当 C 改了 → 需要通知 B ➡️ 如果 B 也没有 HMR 接收器 → 再通知 A(向上冒泡)
# 📡 4. 文件变化后(HMR 整条链路)
下面进入最关键部分:HMR 是怎么真的动起来的?
# ✨ 第 1 步:监听文件变化(chokidar)
在 server/index.ts 中:
watcher.on('change', (file) => {
server.handleHMRUpdate(file)
})
1
2
3
2
3
当你编辑某个文件,比如 src/App.vue:
File changed: src/App.vue
1
# ✨ 第 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
1
如果改了 C.js:
C.js 有没有 accept? ❌ 没有
它的 importer B.js 有没有 accept? ❌ 没有
再向上看 A.vue ✔ 有 Vue 的 HMR accept
➡️ 最终更新 A.vue(触发 Vue 局部更新)
# ✨ 第 4 步:WebSocket 推送更新消息
代码来自 ws.ts:
ws.send({
type: 'update',
updates: [{ path, timestamp }]
})
1
2
3
4
2
3
4
Vite 使用 ws://localhost:5173 建立一个 WebSocket,它只发 JSON:
{
"type": "update",
"updates": [
{
"path": "/src/components/MyComp.vue",
"timestamp": 1679999999999
}
]
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# ✨ 第 5 步:浏览器端(客户端 HMR runtime)
HMR 客户端运行时在:
vite/client
1
主要逻辑:
- 接收 WebSocket push
- 根据
path动态调用import(path + "?t=timestamp") - 替换模块
- 执行
import.meta.hot.accept()回调
示例(简化):
socket.onmessage = async (msg) => {
if (msg.type === 'update') {
for (const update of msg.updates) {
import(update.path + '?t=' + update.timestamp)
}
}
}
1
2
3
4
5
6
7
2
3
4
5
6
7
非常直接:重新 import 文件而已!
没有 patch,没有 diff,没有 bundle —— 所以快。
# ✨ 第 6 步:如果没有模块接受更新 → 整页刷新
location.reload()
1
这就是为什么有时候 HMR 失败,会自动刷新页面。
# 🧩 5. 为什么 Vite 这么快?(总结)
# 因为:
- 使用浏览器原生 ESM (No Bundle)
- 改哪个文件就更新哪个
- HMR 是基于依赖图的精准更新
- 客户端仅需要重新 import 新模块
- 无需 patch, 无需 rebuild, 无需 Pre-bundle
这就是 Vite 能秒级热更新的根本原因。
上次更新: 2025/12/09, 03:15:37