soft invalidation
是vite处理热更新的时候优化方式,首先看看官方对soft invalidation
的解释:链接。
案例
如果没看懂可以看个案例,案例中是个vite
创建的vue
项目,项目主要代码如下:
javascript
// src/app.vue
<script setup lang="ts">
import { foo } from './foo'
</script>
<template>
{{ foo }}
</template>
<style></style>
// src/foo.ts
export const foo = ['foo', 'bar']
当我修改foo.ts后会触发热更新,热更新第一步会执行一个moduleGraph.invalidateModule()方法
typescript
// vite4.5.1中的 moduleGraph.invalidateModule
export class ModuleGraph {
invalidateModule(
mod: ModuleNode,
seen: Set<ModuleNode> = new Set(),
timestamp: number = Date.now(),
isHmr: boolean = false,
hmrBoundaries: ModuleNode[] = []
): void {
// ... 省略
// Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline
// Invalidating the transform result is enough to ensure this module is re-processed next time it is requested
mod.transformResult = null
mod.ssrTransformResult = null
mod.ssrModule = null
mod.ssrError = null
// ... 省略
}
}
如果对moduleNode,moduleGraph不熟悉的可以参考这篇文章
这个方法是清空掉foo.ts对应moduleNode中的transformResult
属性,以及所有导入foo.ts文件中的transformResult
,在上面案例中就是app.vue。
我们来看看transformResult
在vite中的作用
开发环境中,浏览器请求到src/foo.ts(地址简写了)文件时候会发生如下过程:
对应源码简要概括:
typescript
// vite 4.5.1 中的doTransform
async function doTransform(
url: string,
server: ViteDevServer,
options: TransformOptions,
timestamp: number,
) {
url = removeTimestampQuery(url)
const { config, pluginContainer } = server
const prettyUrl = debugCache ? prettifyUrl(url, config.root) : ''
const ssr = !!options.ssr
const module = await server.moduleGraph.getModuleByUrl(url, ssr)
// check if we have a fresh cache
// 检查是否有缓存
const cached =
module && (ssr ? module.ssrTransformResult : module.transformResult)
if (cached) {
// TODO: check if the module is "partially invalidated" - i.e. an import
// down the chain has been fully invalidated, but this current module's
// content has not changed.
// in this case, we can reuse its previous cached result and only update
// its import timestamps.
debugCache?.(`[memory] ${prettyUrl}`)
return cached
}
// 重新加载模块
const resolved = module
? undefined
: (await pluginContainer.resolveId(url, undefined, { ssr })) ?? undefined
// resolve
const id = module?.id ?? resolved?.id ?? url
const result = loadAndTransform(
id,
url,
server,
options,
timestamp,
module,
resolved,
)
getDepsOptimizer(config, ssr)?.delayDepsOptimizerUntil(id, () => result)
return result
}
transformResult
是moduleNode用来储存其对应模块代码。简单来说就是当一个模块被vite钩子load
(加载),transform
(代码转换)处理后,得到了最终代码code,该moduleNode
的transformResult
属性值就是code。
设计transformResult
的原因是为了缓存本次处理模块的结果,如果没有发生变化,下次请求模块的时候会先去访问模块对应的moduleNode
的transformResult
属性,如果有则直接使用,没有则去加载模块。
再回到刚才的案例上面, 我们修改了src/foo.ts后,vite中会执行moduleGraph.invalidateModule()
,其对应的moduleNode
中的transformResult
就会被清空,下次再请求的时候才能请求到最新的src/foo.ts中的内容。
moduleGraph.invalidateModule()
不仅仅会清空src/foo.ts中transformResult内容,还会清空导入src/foo.ts中父模块内容。
typescript
// vite4.5.1中的 moduleGraph.invalidateModule
invalidateModule(
mod: ModuleNode,
seen: Set<ModuleNode> = new Set(),
timestamp: number = Date.now(),
isHmr: boolean = false,
hmrBoundaries: ModuleNode[] = []
)
{
// ... 省略
mod.importers.forEach((importer) => {
if (!importer.acceptedHmrDeps.has(mod)) {
this.invalidateModule(importer, seen, timestamp, isHmr)
}
})
}
产生的问题:
如果很多文件都导入了foo.ts,那么如果foo.ts修改了,如上图所示,所有的对应的导入文件都会重新加载。
解决办法
为了避免导入文件重新加载的问题, vite5.0实现了soft invalidate
。
还是上面的例子,一旦修改了foo.ts,会发生如下过程:
- foo.ts 改变,
invalidate
执行,foo的transformResult被置空。同时moduleNode
中lastHMRTimestamp
会被赋值为当前时间。 - 找到导入foo.ts的文件App.vue,发现此时App.vue中静态导入了foo.ts(就是import {xx} from './foo.ts'),此时App.vue就会被定义为soft invalidate状态。
- vite出发热更新,此时会去请求App.vue文件,此时检测到App.vue是soft invalidate状态,App.vue会继续使用上一次transformResult的结果,并且会修改
transformResult
, 会给所有静态导入模块加上请求时间戳(例如?t=xxxx),这个时间戳就是第一步中设置的lastHMRTimestamp
。 - 请求最新的foo.ts/?t=xxx文件。
整个过程中,通过给静态导入的模块更新其最新的请求时间戳的方式,避免了模块重新加载问题。
源码
invalidateModule
invalidateModule
方法中形参添加softInvalidate
参数,根据它来判断当前是否soft invalidate
。
typescript
// vite 5.1.10
invalidateModule(
mod: ModuleNode,
seen: Set<ModuleNode> = new Set(),
timestamp: number = Date.now(),
isHmr: boolean = false,
/** @internal */
softInvalidate = false,
): void {
const prevInvalidationState = mod.invalidationState
const prevSsrInvalidationState = mod.ssrInvalidationState
// Handle soft invalidation before the `seen` check, as consecutive soft/hard invalidations can
// cause the final soft invalidation state to be different.
// If soft invalidated, save the previous `transformResult` so that we can reuse and transform the
// import timestamps only in `transformRequest`. If there's no previous `transformResult`, hard invalidate it.
// 一个模块要soft invalidate的话,那么必须要有之前就有transformResult 且 传参softInvalidte 为true
if (softInvalidate) {
mod.invalidationState ??= mod.transformResult ?? 'HARD_INVALIDATED'
mod.ssrInvalidationState ??= mod.ssrTransformResult ?? 'HARD_INVALIDATED'
}
// If hard invalidated, further soft invalidations have no effect until it's reset to `undefined`
// 非soft invalidte状态
else {
mod.invalidationState = 'HARD_INVALIDATED'
mod.ssrInvalidationState = 'HARD_INVALIDATED'
}
// Skip updating the module if it was already invalidated before and the invalidation state has not changed
// 如果已经失效过,且失效状态都一样
if (
seen.has(mod) &&
prevInvalidationState === mod.invalidationState &&
prevSsrInvalidationState === mod.ssrInvalidationState
) {
return
}
seen.add(mod)
if (isHmr) {
mod.lastHMRTimestamp = timestamp
} else {
// Save the timestamp for this invalidation, so we can avoid caching the result of possible already started
// processing being done for this module
mod.lastInvalidationTimestamp = timestamp
}
// Don't invalidate mod.info and mod.meta, as they are part of the processing pipeline
// Invalidating the transform result is enough to ensure this module is re-processed next time it is requested
mod.transformResult = null
mod.ssrTransformResult = null
mod.ssrModule = null
mod.ssrError = null
mod.importers.forEach((importer) => {
if (!importer.acceptedHmrDeps.has(mod)) {
// If the importer statically imports the current module, we can soft-invalidate the importer
// to only update the import timestamps. If it's not statically imported, e.g. watched/glob file,
// we can only soft invalidate if the current module was also soft-invalidated. A soft-invalidation
// doesn't need to trigger a re-load and re-transform of the importer.
// 这边是判断导入模块是否soft invalidate 的方式
// 1. 导入模块中导入当前模块是静态导入方式
// App.vue 中 import { foo } from './foo.ts', 此时App.vue就是soft invalidate状态
// 2. 当前模块已经是soft invalite了,那么其导入模块也是soft invalidate方式
// App.vue是soft invalidate, 其导入模块也是soft invalidte方式
const shouldSoftInvalidateImporter =
importer.staticImportedUrls?.has(mod.url) || softInvalidate
this.invalidateModule(
importer,
seen,
timestamp,
isHmr,
shouldSoftInvalidateImporter,
)
}
})
}
上面代码中,我们把当前的transormResult
存储到moduleNode.invalidationState
中,等下vite加载模块的时候也是通过invalidationState
来判断当前模块是否是soft invalidate
状态。
doTransform
typescript
async function doTransform(
url: string,
server: ViteDevServer,
options: TransformOptions,
timestamp: number,
) {
url = removeTimestampQuery(url)
const { config, pluginContainer } = server
const prettyUrl = debugCache ? prettifyUrl(url, config.root) : ''
const ssr = !!options.ssr
const module = await server.moduleGraph.getModuleByUrl(url, ssr)
// tries to handle soft invalidation of the module if available,
// returns a boolean true is successful, or false if no handling is needed
const softInvalidatedTransformResult =
module &&
(await handleModuleSoftInvalidation(module, ssr, timestamp, server))
if (softInvalidatedTransformResult) {
debugCache?.(`[memory-hmr] ${prettyUrl}`)
return softInvalidatedTransformResult
}
// check if we have a fresh cache
const cached =
module && (ssr ? module.ssrTransformResult : module.transformResult)
if (cached) {
debugCache?.(`[memory] ${prettyUrl}`)
return cached
}
// ... 和vite4.5.1中一致 省略
}
vite5.0 在加载模块内容的时候,在之前判断缓存的基础上追加了soft invalidate
的判断。
当一个模块是soft invalidate
的时候,那么就要去更新其中静态导入模块的时间戳。
时间戳更新完毕后,如有结果则会返回当前的结果。
处理静态导入模块时间戳的方法是handleModuleSoftInvalidation
, 其代码如下:
typescript
async function handleModuleSoftInvalidation(
mod: ModuleNode,
ssr: boolean,
timestamp: number,
server: ViteDevServer,
) {
// 是否能进行soft invalidate
const transformResult = ssr ? mod.ssrInvalidationState : mod.invalidationState
// Reset invalidation state
// 重置状态
if (ssr) mod.ssrInvalidationState = undefined
else mod.invalidationState = undefined
// Skip if not soft-invalidated
// 此时不能soft invalidte
if (!transformResult || transformResult === 'HARD_INVALIDATED') return
if (ssr ? mod.ssrTransformResult : mod.transformResult) {
throw new Error(
`Internal server error: Soft-invalidated module "${mod.url}" should not have existing transform result`,
)
}
// 能够soft invalidte
let result: TransformResult
// For SSR soft-invalidation, no transformation is needed
// ssr 过程无需考虑
if (ssr) {
result = transformResult
}
// For client soft-invalidation, we need to transform each imports with new timestamps if available
// 修改url
else {
await init
const source = transformResult.code
const s = new MagicString(source)
const [imports] = parseImports(source)
for (const imp of imports) {
let rawUrl = source.slice(imp.s, imp.e)
if (rawUrl === 'import.meta') continue
const hasQuotes = rawUrl[0] === '"' || rawUrl[0] === "'"
if (hasQuotes) {
rawUrl = rawUrl.slice(1, -1)
}
const urlWithoutTimestamp = removeTimestampQuery(rawUrl)
// hmrUrl must be derived the same way as importAnalysis
// 处理hmrUrl
const hmrUrl = unwrapId(
stripBase(removeImportQuery(urlWithoutTimestamp), server.config.base),
)
// 找到所有的静态导入的模块并更新其时间戳
for (const importedMod of mod.clientImportedModules) {
if (importedMod.url !== hmrUrl) continue
if (importedMod.lastHMRTimestamp > 0) {
const replacedUrl = injectQuery(
urlWithoutTimestamp,
`t=${importedMod.lastHMRTimestamp}`,
)
const start = hasQuotes ? imp.s + 1 : imp.s
const end = hasQuotes ? imp.e - 1 : imp.e
// 修改导入的url
s.overwrite(start, end, replacedUrl)
}
if (imp.d === -1 && server.config.server.preTransformRequests) {
// pre-transform known direct imports
server.warmupRequest(hmrUrl, { ssr })
}
break
}
}
// Update `transformResult` with new code. We don't have to update the sourcemap
// as the timestamp changes doesn't affect the code lines (stable).
// 修改transformResult, sourcemap无需修改 transformResult加了时间戳,代码行数不影响
const code = s.toString()
result = {
...transformResult,
code,
etag: getEtag(code, { weak: true }),
}
}
// Only cache the result if the module wasn't invalidated while it was
// being processed, so it is re-processed next time if it is stale
// 当前时间大于上次更新时间 则存储下当前的处理结果
if (timestamp > mod.lastInvalidationTimestamp) {
if (ssr) mod.ssrTransformResult = result
else mod.transformResult = result
}
return result
}
总结
vite5.0中通过针对静态导入的模块,通过更新导入url时间戳方式,避免了模块重新加载。
上面内容如果有错误,请欢迎指出~~☺️