theme: cyanosis
highlight: monokai
前置
经过前面几篇铺垫之后,接下来看下 Vite 的热更新原理,先看下前置流程图
接下来会详细分析下上图的过程
demo
假设main.ts
文件如下
typescript import a from './a' console.log(a) if(import.meta.hot){ import.meta.hot.accept('./a', a => { console.log('hmr', a) }) }
请求 main.ts
请求main.ts
时,如果main.ts
中存在import.meta.hot.accept
。经importAnalysisPlugin
编译后,会向这个文件中添加一段代码
typescript import { createHotContext as __vite__createHotContext } from "/@vite/client"; import.meta.hot = __vite__createHotContext(url/* 从项目根路径查找的当前文件的绝对路径 */);
当客户端执行main.ts
时,调用/@vite/client
的createHotContext
函数
监听服务器返回的消息
/@vite/client
路径解析之后对应源码的位置就是/src/client/client.ts
``typescript const socketProtocol = __HMR_PROTOCOL__ || (location.protocol === 'https:' ? 'wss' : 'ws') const socketHost =
${HMRHOSTNAME || location.hostname}:${HMRPORT} // 创建 WebSocket 对象 const socket = new WebSocket(
${socketProtocol}://${socketHost}`, 'vite-hmr') const base = BASE || '/'
// Listen for messages socket.addEventListener('message', async ({ data }) => {}) `` 首先会创建一个
WebSocket对象,并注册监听事件,也就是说当服务器通过
WebSocket`返回信息时,会被这个事件捕获。
还会定义一些变量,这些变量如下 ```typescript /* * 存储的是被请求文件的 import.meta.hot.accept * key: 当前文件地址 * value: { id: 文件地址, callbacks: [{ deps: 被监听的文件路径数组, fn: 定义的回调函数 }] } */ const hotModulesMap = new Map () / * * 存储的是所有被请求文件的 import.meta.hot.dispose * key: 当前文件地址 * value: 定义的回调函数 / const disposeMap = new Map >() / * * 存储的是所有被请求文件的 import.meta.hot.prune * key: 当前文件地址 * value: 定义的回调函数 / const pruneMap = new Map >() / * * 存储的是所有被请求文件的 import.meta.hot.data * 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。 * key: 当前文件地址 * value: 是一个对象,持久化的信息 / const dataMap = new Map () / * * 存储的是被请求文件监听的 hmr 钩子 * key: 事件名称 * value: 事件对应回调函数的数组 / const customListenersMap = new Map * * 存储的是被请求文件监听的 hmr 钩子 * key: 当前文件地址 * value: 是一个 Map,Map 内的 key 是事件名,valye 是一个回调函数数组 */ const ctxToListenersMap = new Map
export const createHotContext = (ownerPath: string) => {} ``
customListenersMap和
ctxToListenersMap`在最后介绍自定义钩子时会介绍
创建 import.meta.hot 对象
继续向下,导出 createHotContext
函数
上面说过,对于存在import.meta.hot.accept
的模块会执行这个函数,并传入当前文件的绝对路径。 typescript import { createHotContext as __vite__createHotContext } from "/@vite/client"; import.meta.hot = __vite__createHotContext(url/* 从项目根路径查找的当前文件的绝对路径 */);
createHotContext
函数如下 ```typescript // ownerPath 当前文件的路径 export const createHotContext = (ownerPath: string) => { // 如果 dataMap 中没有当前路径,则添加到 dataMap 中 if (!dataMap.has(ownerPath)) { dataMap.set(ownerPath, {}) }
// when a file is hot updated, a new context is created
// clear its stale callbacks
const mod = hotModulesMap.get(ownerPath)
if (mod) {
// 清空 cb
mod.callbacks = []
}
// 清除过时的自定义事件监听器(最后介绍自定义钩子时会介绍)
// ...
const newListeners = new Map()
ctxToListenersMap.set(ownerPath, newListeners)
function acceptDeps(){}
const hot = {
get data() {},
accept(deps: any, callback?: any) {},
acceptDeps() {},
dispose(cb: (data: any) => void) {},
prune(cb: (data: any) => void) {},
// TODO
decline() {},
invalidate() {},
on: (event: string, cb: (data: any) => void) => {},
}
return hot
} `` 这个函数最主要的作用就是定义一个
hot对象,并返回这个对象。返回的对象会赋值给模块的
import.meta.hot`
总结下createHotContext
函数的作用: - 创建持久化数据对象,并添加到dataMap
中 - 清空hotModulesMap
内当前路径绑定的依赖回调(accept
函数的参数) - 清除过时的自定义事件监听器 - 给当前路径创建自定义事件回调的容器,并放到ctxToListenersMap
中
执行import.meta.hot.accept
方法
在这里主要分析下accept
方法的作用,其他方法都比较简单这里就不赘述了,可以直接去源码中看。
typescript accept(deps: any, callback?: any) { if (typeof deps === 'function' || !deps) { // self-accept: hot.accept(() => {}) acceptDeps([ownerPath], ([mod]) => deps && deps(mod)) } else if (typeof deps === 'string') { // explicit deps acceptDeps([deps], ([mod]) => callback && callback(mod)) } else if (Array.isArray(deps)) { acceptDeps(deps, callback) } else { throw new Error(`invalid hot.accept() usage.`) } },
代码中,对import.meta.hot.accept
不同的参数形式做了不同处理,分别是 - hot.accept(() => {})
接收自身 - hot.accept('./a', () => {})
接受一个直接依赖项的更新 - hot.accept(['./a', './b'], () => {})
接受多个直接依赖项的更新
最后都是调用acceptDeps
方法,参数为接收文件的数组、对应文件更新时的回调 typescript function acceptDeps(deps: string[], callback: HotCallback['fn'] = () => {}) { const mod: HotModule = hotModulesMap.get(ownerPath) || { id: ownerPath, callbacks: [], } mod.callbacks.push({ deps, fn: callback, }) // 设置 hotModulesMap hotModulesMap.set(ownerPath, mod) }
acceptDeps
方法的作用就是将文件更新时的回调函数添加到hotModulesMap
中,存储的结构已经在上面说过
到此前置工作就已经做完了,接下来就是文件更新流程
更新
假设main.ts
代码如下 typescript import a from './a' console.log(a) if(import.meta.hot){ import.meta.hot.accept('./a', a => { console.log('hmr', a) }) }
当更新a.ts
的内容时,会被chokidar
注册的监听捕获 typescript // file:被修改文件的绝对路径 watcher.on('change', async (file) => { file = normalizePath(file) // 清空被修改文件对应的ModuleNode对象的 transformResult 属性 // 这个属性存储的是编译后的代码内容 moduleGraph.onFileChange(file) if (serverConfig.hmr !== false) { try { await handleHMRUpdate(file, server) } catch (err) {} } })
当文件修改被chokidar
注册的监听器捕获之后,根据被修改文件的路径清空对应的ModuleNode中缓存的源码。然后调用handleHMRUpdate
函数,开始热更新流程
```typescript export async function handleHMRUpdate( file: string, server: ViteDevServer ): Promise { const { ws, config, moduleGraph } = server // 获取 file 相对于根路径的相对路径 const shortFile = getShortName(file, config.root) // 如果当前文件是配置文件则为 true const isConfig = file === config.configFile // 如果当前文件名是自定义的插件则为 true const isConfigDependency = config.configFileDependencies.some( (name) => file === path.resolve(name) ) //如果是环境变量文件,则为 true const isEnv = config.inlineConfig.envFile !== false && file.endsWith('.env') // 环境变量文件、自定义插件、配置文件修改会重启服务 if (isConfig || isConfigDependency || isEnv) { await restartServer(server) return }
// /xxx/node_modules/vite/dist/client
// 如果是客户端使用的热更新文件,重新加载页面
if (file.startsWith(normalizedClientDir)) {
ws.send({
type: 'full-reload',
path: '*',
})
return
}
// 根据文件绝对路径获取 ModuleNode 对象(Set)
// 是一个数组,因为会出现单个文件可能映射到多个服务模块,比如 Vue单文件组件
const mods = moduleGraph.getModulesByFile(file)
const timestamp = Date.now()
const hmrContext: HmrContext = {
file,
timestamp,
modules: mods ? [...mods] : [],
read: () => readModifiedFile(file),
server,
}
// 调用所有插件定义的 handleHotUpdate 钩子函数
for (const plugin of config.plugins) {
if (plugin.handleHotUpdate) {
const filteredModules = await plugin.handleHotUpdate(hmrContext)
if (filteredModules) {
hmrContext.modules = filteredModules
break
}
}
}
if (!hmrContext.modules.length) {
// 如果是 html 文件 重新加载页面
if (file.endsWith('.html')) {
ws.send({
type: 'full-reload',
path: config.server.middlewareMode
? '*'
: '/' + normalizePath(path.relative(config.root, file)),
})
} else {}
return
}
updateModules(shortFile, hmrContext.modules, timestamp, server)
} `` 整体流程如下 - 配置文件更新、
.env更新、自定义插件或引入配置文件的自定义文件更新都会重起服务器 - 客户端使用的热更新文件更新、
index.html更新,重新加载页面 - 调用所有插件定义的
handleHotUpdate钩子函数,具体功能可参考[文档](https://vitejs.cn/guide/api-plugin.html#handlehotupdate) - 过滤和缩小受影响的模块列表,使 HMR 更准确。 - 返回一个空数组,并通过向客户端发送自定义事件来执行完整的自定义 HMR 处理 - 如果是其他文件更新,调用
updateModules`函数
先大体看下updateModules
函数做了什么事 ```typescript function updateModules( file: string, modules: ModuleNode[], timestamp: number, { config, ws }: ViteDevServer ) { const updates: Update[] = [] const invalidatedModules = new Set
() // 是否需要重新加载 let needFullReload = false
// 遍历该文件编译后的所有文件
for (const mod of modules) {}
if (needFullReload) {
ws.send({
type: 'full-reload',
})
} else {
ws.send({
type: 'update',
updates,
})
}
} `` 首先定义一个
updates数组,用于存储要更新的模块;定义一个
needFullReload变量,如果为
true则重新加载整个页面。然后遍历传入的
modules。最后根据
needFullReload的值判断是重新加载整个页面还是更新某些模块
updates`。
由此可以看出循环的作用就是根据修改模块获取整个更新链路。
```typescript // 遍历该文件编译后的所有文件 for (const mod of modules) { // 沿着引用路线向上查找,设置时间戳、清空 transformResult invalidate(mod, timestamp, invalidatedModules) // 如果需要重新加载,不需要再遍历其他的了 if (needFullReload) { continue }
const boundaries = new Set<{
boundary: ModuleNode
acceptedVia: ModuleNode
}>()
// 查找引用模块,判断是否需要重载页面
const hasDeadEnd = propagateUpdate(mod, boundaries)
if (hasDeadEnd) {
// 如果 hasDeadEnd 为 true,则全部更新。
needFullReload = true
continue
}
updates.push(
...[...boundaries].map(({ boundary, acceptedVia }) => ({
type: `${boundary.type}-update` as Update['type'], // 更新类型
timestamp, // 时间戳
path: boundary.url, // 依赖该文件的文件
acceptedPath: acceptedVia.url, // 当前文件
}))
)
} `` 首先调用
invalidate,这个函数的作用就是一层一层向上查找,并修改
lastHMRTimestamp和清空
transformResult;如果上层模块不接受当前模块的热更新;则继续调用
invalidate,并传入上层模块对应的
ModuleNode对象。检测上上层是否接受上层模块热更新;并修改上层模块对应的
ModuleNode对象的
lastHMRTimestamp、
transformResult`对象。如果上上层也没有接受,再往上找。
这样做的目的是,对于上层模块来说,如果没有监听子模块更新,当子模块更新时,上层模块也需要重新加载。此时需要更新时间戳和清空缓存的代码,防止再次返回缓存的代码。
如果监听了子模块更新,就不需要更新自身了,而是可以通过监听的回调重新执行子模块导出的内容。所以就不需要更新时间戳和清空代码了。 typescript function invalidate(mod: ModuleNode, timestamp: number, seen: Set<ModuleNode>) { // 防止死循环 if (seen.has(mod)) { return } seen.add(mod) // 设置修改时间 mod.lastHMRTimestamp = timestamp mod.transformResult = null // 遍历导入该文件的所有文件 mod.importers.forEach((importer) => { // 如果上层文件的 acceptedHmrDeps 上不包含当前文件,说明上层文件没有定义接受当前文件更新的回调 // 则再次对上层文件调用 invalidate 方法 if (!importer.acceptedHmrDeps.has(mod)) { invalidate(importer, timestamp, seen) } }) }
上述操作做完之后,继续向下执行定义一个boundaries
变量,并调用propagateUpdate
函数,参数是当前模块对应的ModuleNode
对象和boundaries
变量 ```typescript function propagateUpdate( node: ModuleNode, boundaries: Set<{ boundary: ModuleNode acceptedVia: ModuleNode }>, currentChain: ModuleNode[] = [node] ): boolean /* hasDeadEnd */ { // 如果是自身监听,则添加到 boundaries 中,并返回 false if (node.isSelfAccepting) { boundaries.add({ boundary: node, acceptedVia: node, }) // ...
return false
}
// 当前模块没有被任何模块导入,返回 true,即全部更新
if (!node.importers.size) {
return true
}
// 当前文件不是 css 文件,并且只有css文件导入了当前模块,返回 true
if (!isCSSRequest(node.url) && [...node.importers].every((i) => isCSSRequest(i.url))) {
return true
}
// 遍历导入此模块的所有模块对象,向上查找
for (const importer of node.importers) {
const subChain = currentChain.concat(importer)
// 如果当前模块的上层模块接收当前模块的更新,则添加到 boundaries 中
if (importer.acceptedHmrDeps.has(node)) {
boundaries.add({
boundary: importer, // 导入当前模块的模块对象
acceptedVia: node, // 当前模块
})
continue
}
// 重复引入,如果不 return 出去,则会造成死循环
if (currentChain.includes(importer)) {
return true
}
// 递归调用 propagateUpdate,收集 boundaries,向上查找
if (propagateUpdate(importer, boundaries, subChain)) {
return true
}
}
return false
} `` 这个函数的作用就是获取要更新的所有模块,并判断是否要重新加载整个页面。沿着导入链向上查找,直到找到接收自更新或者接收子模块更新的模块,将这个模块添加到
boundaries中;并返回
true,反之返回
false`
- 假设有 A、B、C、D 四个模块,他们的引用关系是 A -> B -> C -> D ,其中模块 A 接收模块 B 更新;当修改模块 D 时,返回
false
,并将 模块A 收集到boundaries
中 - 假设有 A、B、C、D 四个模块,他们的引用关系是 A -> B -> C -> D ,其中模块 C 接收模块 D 更新;当修改模块 D 时,返回
false
,并将 模块C 收集到boundaries
中 - 假设有 A、B、C、D 四个模块,他们的引用关系是 A -> B -> C -> D ,所有模块都不接受热更新;当修改模块 D 时,返回
true
,boundaries
为空
回到updateModules
函数,将收集到的模块数组和updates
合并 typescript updates.push( ...[...boundaries].map(({ boundary, acceptedVia }) => ({ type: `${boundary.type}-update` as Update['type'], // 更新类型 js/css timestamp, // 时间戳 path: boundary.url, // 导入该模块的模块 acceptedPath: acceptedVia.url, // 当前模块 })) )
循环完成之后,将消息发送给客户端。根据needFullReload
判断更新方式;如果propagateUpdate
返回true
,说明需要重新加载页面。反之就是更新模块。 typescript if (needFullReload) { ws.send({ type: 'full-reload', }) } else { ws.send({ type: 'update', updates, }) }
总流程如下
客户端接收消息
先看下流程图
前面分析过,客户端注册了 WebSocket 监听 typescript socket.addEventListener('message', async ({ data }) => { handleMessage(JSON.parse(data)) })
当客户端接收到服务器发过来的消息后,调用handleMessage
函数 ``typescript async function handleMessage(payload: HMRPayload) { switch (payload.type) { case 'connected': console.log(
[vite] connected.`) setInterval(() => socket.send('ping'), HMR_TIMEOUT) break case 'update': // 调用 vite:beforeUpdate 事件的回调 notifyListeners('vite:beforeUpdate', payload) // ...
payload.updates.forEach((update) => {
if (update.type === 'js-update') {
queueUpdate(fetchUpdate(update))
} else {/* ... */}
})
break
case 'custom': {
// ...
break
}
case 'full-reload':
// 调用 vite:beforeFullReload 事件的回调
notifyListeners('vite:beforeFullReload', payload)
if (payload.path && payload.path.endsWith('.html')) {
// if html file is edited, only reload the page if the browser is currently on that page.
const pagePath = location.pathname
const payloadPath = base + payload.path.slice(1)
if (pagePath === payloadPath || (pagePath.endsWith('/') && pagePath + 'index.html' === payloadPath)) {
location.reload()
}
return
} else {
location.reload()
}
break
case 'prune':
// ...
break
case 'error': {/* ... */}
default: {/* ... */}
}
} ``
handleMessage根据服务器传入的
type走不同逻辑,这里我们只看
update`逻辑的。
遍历需要更新的模块数组,如果是js-update
类型,对这个模块执行queueUpdate(fetchUpdate(update))
```typescript async function fetchUpdate({ path, acceptedPath, timestamp }: Update) { // path 接收热更新的模块 // mod:{ id: 文件地址, callbacks: [{ deps: 被监听的文件路径数组, fn: 定义的回调函数 }] } const mod = hotModulesMap.get(path) if (!mod) { return }
const moduleMap = new Map()
// 如果是自身更新
const isSelfUpdate = path === acceptedPath
// make sure we only import each dep once
const modulesToUpdate = new Set<string>()
if (isSelfUpdate) {
// self update - only update self
modulesToUpdate.add(path)
} else {
// deps 中存储的是当前模块接受的直接依赖项
// 这块代码逻辑是说,如果当前模块接收的依赖项中包含 acceptedPath 模块,则将这个路径添加到 modulesToUpdate 中
for (const { deps } of mod.callbacks) {
deps.forEach((dep) => {
if (acceptedPath === dep) {
modulesToUpdate.add(dep)
}
})
}
}
// 获取符合条件的回调
// 过滤条件是如果 mod.callbacks.deps 中的元素在 modulesToUpdate 中存在,则返回 true
const qualifiedCallbacks = mod.callbacks.filter(({ deps }) => {
return deps.some((dep) => modulesToUpdate.has(dep))
})
await Promise.all(
Array.from(modulesToUpdate).map(async (dep) => {
// 获取模块设置的副作用中的回调 import.meta.hot.dispose
const disposer = disposeMap.get(dep)
// import.meta.hot.data 对象在同一个更新模块的不同实例之间持久化。它可以用于将信息从模块的前一个版本传递到下一个版本。
// 调用 disposer
if (disposer) await disposer(dataMap.get(dep))
const [path, query] = dep.split(`?`)
try {
// 拼接路径,请求新的文件
const newMod = await import(
base + path.slice(1) + `?import&t=${timestamp}${query ? `&${query}` : ''}`
)
// 文件的导出内容添加到 moduleMap 中
moduleMap.set(dep, newMod)
} catch (e) {}
})
)
return () => {
for (const { deps, fn } of qualifiedCallbacks) {
// 调用 import.meta.hot.accept 中定义的回调函数,并将文件的导出内容传入回调函数中
fn(deps.map((dep) => moduleMap.get(dep)))
}
const loggedPath = isSelfUpdate ? path : `${acceptedPath} via ${path}`
console.log(`[vite] hot updated: ${loggedPath}`)
}
} ``
fetchUpdate函数的作用就是收集需要更新的模块路径和
import.meta.hot.accept中的回调函数,调用
import.meta.hot.disposer的回调函数清空副作用;拼接更新模块的路径,会**挂上
import和时间戳**;通过
import()加载拼接后的路径。最后返回一个函数,这个函数的作用就是
import.meta.hot.accept`中的回调函数,并打印更新信息。
返回的这个函数被queueUpdate
函数接收;queueUpdate
函数内会收集fetchUpdate
函数返回的函数;并在下一个任务队列中触发所有回调。 typescript // fetchUpdate 函数返回的函数,函数内调用 import.meta.hot.accept 内定义的回调函数,并将请求文件的导出内容当作参数传入 async function queueUpdate(p: Promise<(() => void) | undefined>) { queued.push(p) if (!pending) { pending = true await Promise.resolve() pending = false const loading = [...queued] queued = [] ;(await Promise.all(loading)).forEach((fn) => fn && fn()) } }
总结
Vite 的热更新原理可以大体总结为下面几步
前置 - 服务器启动时创建 WebSocket 实例、通过 chokidar 监听文件修改 - 对请求的 HTML 文件注入客户端热更新代码 - 加载客户端热更新代码时,创建 WebSocket 实例,并注册监听 - 当被请求文件中有import.meta.hot.accept
时,向该文件注入import.meta.hot.accept
定义
更新
- 当文件更新时,触发文件修改的回调。
- 如果是配置文件、自定义插件、
.env
文件修改直接重启服务器 - 反之,根据模块路径向上查找;收集接受当前依赖项更新的模块,并判断是否是刷新页面
- 如果是刷新页面,向客户端发送刷新页面的消息,反之发送更新消息,并将接受当前依赖项更新的模块一起发送给客户端
- 客户端接收到之后,获取要更新的模块路径和热更新回调,通过
import()
请求要更新模块的路径,并在 URL 挂上import
和时间戳;并在下一任务队列中触发热更新回调。
举例
假设有 A、B、C、D 四个模块,他们的引用关系是 A -> B -> C -> D
有模块接受依赖项更新
- 其中模块 A 接收模块 B 更新;当修改模块 D 时,此时是局部更新,修改模块B、C、D的时间戳并清空源码缓存;将 模块A 收集到
boundaries
中。服务器返回的消息中,包含模块A的相关信息。客户端接收到消息后,查找模块A接收热更新的模块。也就是模块B。拼接模块B的路径并重新请求模块B。Vite会将模块B内的导入路径(模块C)挂上t
参数,从而强制浏览器重新请求。模块B返回后,请求模块C,也会给导入路径(模块D)挂上t
参数。最后调用模块A中的热更新回调。 - 其中模块 C 接收模块 D 更新;当修改模块 D 时,此时是局部更新,修改模块D的时间戳并清空源码缓存;将 模块C 收集到
boundaries
中。服务器返回的消息中,包含模块C的相关信息。客户端接收到消息后,查找模块C接收热更新的模块。也就是模块D。拼接模块D的路径并重新请求模块D。模块D返回后,调用模块C中的热更新回调。
模块接收自更新
- 假设模块 D 接收自更新;当修改模块 D 时,此时也是局部更新,并将 模块D 自身收集到
boundaries
中。服务器返回的消息中,包含模块D的相关信息。客户端接收到消息后,由于是接收自更新,所以查找模块D。拼接模块D的路径并重新请求模块D。模块D返回后,调用模块D中的热更新回调。 - 假设模块A接收自更新;当修改模块 D 时,此时是局部更新,,修改模块B、C、D的时间戳并清空源码缓存;将 模块A 收集到
boundaries
中。服务器返回的消息中,包含模块A的相关信息。客户端接收到消息后,由于是接收自更新,所以查找模块A。拼接模块A的路径并重新请求模块A。也是会修改导入模块B、C、D的路径挂上t
参数。模块A返回后,调用模块A中的热更新回调。
没有模块接收热更新
当修改模块D时,由于没有模块接收热更新,所以会直接像客户端发送页面重新加载的消息,客户端接收到之后,直接刷新页面。
自定义钩子
Vite 的 HMR 有4个自定义钩子,分别在不同时机自动触发:
'vite:beforeUpdate'
当更新即将被应用时(例如,一个模块将被替换)'vite:beforeFullReload'
当完整的重载即将发生时'vite:beforePrune'
当不再需要的模块即将被剔除时'vite:error'
当发生错误时(例如,语法错误)
也可以通过handleHotUpdate
钩子函数注册新的钩子函数。
typescript handleHotUpdate({ server }) { server.ws.send({ type: 'custom', event: 'special-update', data: {} }) return [] }
怎么传入回调
通过import.meta.hot.on
typescript const hot = { on: (event: string, cb: (data: any) => void) => { const addToMap = (map: Map<string, any[]>) => { const existing = map.get(event) || [] existing.push(cb) map.set(event, existing) } addToMap(customListenersMap) addToMap(newListeners) } }
当执行import.meta.hot.on
时,调用了两次addToMap
函数,第一次传入customListenersMap
,第二次传入newListeners
。并将传入的回调放到这两个变量里面。
customListenersMap
是在执行/@vite/client
模块创建的
typescript /** * 存储的是被请求文件监听的 hmr 钩子 * key: 事件名称 * value: 事件对应回调函数的数组 */ const customListenersMap = new Map<string, ((data: any) => void)[]>()
newListeners
是在执行createHotContext
函数创建的。
typescript const newListeners = new Map() ctxToListenersMap.set(ownerPath, newListeners)
也就是说newListeners
最终存储在ctxToListenersMap
中
```typescript /** * 存储的是被请求文件监听的 hmr 钩子 * key: 当前文件地址 * value: 是一个 Map,Map 内的 key 是事件名,valye 是一个回调函数数组 */ const ctxToListenersMap = new Map< string, Map
() ```
ctxToListenersMap
和customListenersMap
的区别是:
customListenersMap
存储的结构是:事件名:事件回调数组ctxToListenersMap
存储的结构是:文件名:Map<事件名,事件回调数组>
在执行createHotContext
时,还有一段代码,用于清空过时的事件回调。因为如果当前文件重新请求,会重新创建一个新的Context上下文,之前的就没用了,需要清空。
typescript // 获取当前模块监听的所有事件 const staleListeners = ctxToListenersMap.get(ownerPath) if (staleListeners) { // 遍历当前模块监听的回调 for (const [event, staleFns] of staleListeners) { // 将 staleFns 中所有回调从 customListenersMap 中清除 const listeners = customListenersMap.get(event) if (listeners) { customListenersMap.set( event, listeners.filter((l) => !staleFns.includes(l)) ) } } }
钩子调用时机
客户端监听到服务器消息后会调用handleMessage
钩子函数;
typescript async function handleMessage(payload: HMRPayload) { switch (payload.type) { case 'update': notifyListeners('vite:beforeUpdate', payload) // ... break; case 'custom': { notifyListeners(payload.event as CustomEventName<any>, payload.data) break } case 'full-reload': notifyListeners('vite:beforeFullReload', payload) break; }
上面只是源码的一部分,从上面可以看到当服务器返回不同消息类型时,会调用不同的钩子函数
typescript function notifyListeners(event: string, data: any): void { const cbs = customListenersMap.get(event) if (cbs) { cbs.forEach((cb) => cb(data)) } }
就是从customListenersMap
中根据事件名获取所有回调,然后执行这些回调