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