Skip to content

它是怎么做到“极速 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. ModuleGraph 模块图(追踪模块依赖)
  2. Watcher 文件监听器(chokidar)
  3. WS WebSocket 系统(向浏览器推送更新事件)
  4. 热更新处理器:server.handleHMRUpdate()
  5. 客户端 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.js

ModuleGraph 会记录:

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

它会:

  1. 找到对应的 ModuleNode
  2. 执行该模块的插件 hmr 钩子(例如 Vue 插件)
  3. 采集依赖链中接受更新的模块
  4. 通过 WebSocket 发送更新事件

✨ 第 3 步:确定哪些模块“接受更新”

HMR 是有两种处理方式:

✔ 有 import.meta.hot.accept()

→ 局部热更新(只刷新相关模块)

❌ 没 HMR 处理器

→ 根据依赖反向查找 “importers” → 找到最近的 HMR 边界 → 如果找不到 → 全量刷新页面

例如:

A.vue → import B.js → import C.js

如果改了 C.js

  1. C.js 有没有 accept? ❌ 没有

  2. 它的 importer B.js 有没有 accept? ❌ 没有

  3. 再向上看 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

主要逻辑:

  1. 接收 WebSocket push
  2. 根据 path 动态调用 import(path + "?t=timestamp")
  3. 替换模块
  4. 执行 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 能秒级热更新的根本原因。