Vite 源码(七)Vite 的热更新原理


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/clientcreateHotContext函数

监听服务器返回的消息

/@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) => {} `` customListenersMapctxToListenersMap`在最后介绍自定义钩子时会介绍

创建 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对象的lastHMRTimestamptransformResult`对象。如果上上层也没有接受,再往上找。

这样做的目的是,对于上层模块来说,如果没有监听子模块更新,当子模块更新时,上层模块也需要重新加载。此时需要更新时间戳和清空缓存的代码,防止再次返回缓存的代码。

如果监听了子模块更新,就不需要更新自身了,而是可以通过监听的回调重新执行子模块导出的内容。所以就不需要更新时间戳和清空代码了。 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 时,返回trueboundaries为空

回到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

() ```

ctxToListenersMapcustomListenersMap的区别是:

  • 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中根据事件名获取所有回调,然后执行这些回调

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值