文件定位与整体设计
负责执行状态本身 ,它是 Module Runner 的运行时模块图 + 缓存 ,是 Module Runner 的状态层 ,用三张索引表维护模块执行图,并通过归一化、幂等创建、局部失效和懒解析,为 loader 提供一致、可缓存、可热更新的模块状态视图
服务端也有一个模块图(Vite 的 ModuleGraph),但它记录的是 transform 关系 (谁 import 谁、转换结果、失效标记)。本文件记录的是运行状态:
- 这个模块执行完了吗(
evaluated) - 正在执行中的模块是什么(
promise) - 导出对象是什么(
exports) - 谁
import了我、我import了谁(importers/imports) - 服务端给的元数据、内联
sourcemap缓存(meta/map)
EvaluatedModuleNode
单个模块的运行状态节点
| 字段 | 类型 | 含义与作用 |
|---|---|---|
id |
string(构造参数,只读) |
规范模块 id 。通常是文件系统路径 + query/hash(如 C:/proj/src/a.vue?vue&type=script),也可能是虚拟模块 id(\0virtual:xxx)。它是与服务端模块图对应的键。 |
url |
string(构造参数,只读) |
import 语句中使用的原始 URL (未 resolve),如 /src/a.vue、/@fs/C:/...。用于 full-reload 时重新 import(url)。 |
importers |
Set<string> |
反向边:谁 import 了我(存 id)。HMR 的 full-reload 需要沿它回溯到入口 ;runner.cachedRequest 每次请求都会补充。 |
imports |
Set<string> |
正向边:我 import 了谁(存 id)。循环依赖检测(isCircularRequest)依赖它。 |
evaluated |
boolean |
是否已完成执行。初始 false;cachedRequest 在 finally 中置 true;invalidateModule 重置。 |
meta |
`ResolvedResult | undefined` |
promise |
`Promise |
undefined` |
exports |
any |
模块命名空间对象。在代码执行前就赋值 (directRequest 中),使循环依赖能拿到部分导出;执行完成后被 Object.seal。 |
file |
string |
cleanUrl(id) 的结果,即文件路径(去掉 query/hash)。用于 fileToModulesMap 索引,供 HMR 按文件查找节点。 |
map |
`DecodedMap | undefined` |
设计要点
id和url分开存 :因为"同一个模块"可以通过不同URL到达(/src/a.js、/@fs/...、/@id/...),而服务端只认id。url保留是为了full-reload能重新import原来那个入口URL。file由id派生,让同一文件的多个模块(不同query)能被一起检索到,这正是?raw、?url、?vue&type=script等多实例场景需要的exports的生命周期由runner管理(创建、seal),节点只持有引用;图不关心导出内容,只关心现在有没有
EvaluatedModules 三张索引表
三张表必须同步维护 ,这是本类正确性的核心,所有写操作都集中在 ensureModule / invalidateModule / clear 中
| 表 | key |
value |
服务的场景 |
|---|---|---|---|
idToModuleMap |
规范 id |
节点 | 主索引:执行、循环检测、HMR 回溯 |
fileToModulesMap |
文件路径 | 节点集合 | HMR 收到"某文件变了"时找出所有相关模块(一个文件可有多个 query 变体) |
urlToIdModuleMap |
import 的原始 URL |
节点 | 阶段 1 缓存查找(cachedModule),避免同一 URL 重复 fetch |
方法解析
getModuleById:最简单的按 id 查找。调用方:cachedRequest(取meta)、isCircularRequest、hmrHandler(沿importers回溯)、sourcemap拦截器getModulesByFile:按文件路径取所有 节点。核心调用方是 hmrHandler.ts,full-reload时根据triggeredBy文件找到相关模块,再沿importers递归找入口。"文件 → 多模块"的映射是热更新正确性的基础(否则a.vue?vue&type=script更新了,a.vue主体却收不到)getModuleByUrl:按请求URL获取模块。浏览器/客户端拿到的URL可能是/@id/virtual:foo或/@id/__x00__foo,而ensureModule建节点时用的url是服务端返回的规范形式。unwrapId去掉/@id/前缀并把__x00__还原成\0,保证查得到ensureModule:getModuleInformation每次fetch成功后调用,有则复用、无则创建的幂等语义 ,让任意层级的import(包括并发、循环、重复)最终都收敛到同一节点
- 先归一化 id :保证
node:fs、/@fs/...、file:///...等形式不会产生重复节点 - 已存在则复用,并为新
URL注册别名 :同一个模块用不同URL到达时,url→node表会同时存在多个键,这正是不重复执行的关键 - 不存在则创建,并写入三张表 :文件索引用
node.file(已clean)作为key,把同一文件的多个变体聚到同一个Set中
invalidateModule:重置执行状态,但保留图结构 。用于服务端fetchModule返回invalidate: true(服务端判定客户端缓存已过期)和其他需要强制重新执行的场景
- 清
promise/evaluated/exports→ 下次请求会重新执行 - 清
meta/map→ 下次请求会重新fetch、重新解析sourcemap - 清
imports→ 依赖关系可能已变化,重新执行时重建 - 保留
importers:若清掉反向边,模块在重新执行装入图之前就是"孤立节点"。此时若文件被删除,HMR沿importers找不到任何入口,full-reload静默失效。保留旧边是有意的权衡 ,宁可偶尔多触发一次reload(文件已删除但旧importer仍在),也不能漏掉真实更新
getModuleSourceMapById:懒解析内联sourcemap,供stack trace还原使用
- 节点不存在或无
meta.code→null - 已解析(
mod.map)→ 直接返回 - 用
lastIndexOf(pattern)找最后一个//# sourceMappingURL=data:application/json;base64, - 正则捕获 base64 数据 →
decodeBase64→JSON.parse→ 构造DecodedMap(map, mod.file),DecodedMap(decoder.ts)在构造时会把sources相对mod.file解析成绝对路径(resolvedSources),这样堆栈帧能显示真实源文件 - 写回
mod.map缓存,后续O(1)
clear:清空三张表。被runner.clearCache()和full-reload调用(full-reload会先evaluatedModules.clear()再重新import入口)。不清理各节点内部 ,而是直接丢掉所有节点和索引,让GC回收------比逐个reset简单可靠
辅助方法
normalizeModuleId:归一化
- 同一模块可能以多种
URL形式出现(浏览器URL、/@fs/、node:、file://),归一化后落到同一个节点,避免重复执行和缓存分裂 - 归一化结果与服务端模块图的
id对齐,fetchModule的cached协商、HMR的id匹配都依赖这一点 - 全程用
slash转posix风格,保证逻辑跨平台一致
按顺序:
| 输入形式 | 处理 | 结果 |
|---|---|---|
virtual:custom |
无匹配 | virtual:custom |
\0custom |
无匹配 | \0custom |
node:fs |
去 node: |
fs |
/@fs/C:/root/id.js(Windows) |
去 /@fs/ 且连根斜杠一起去 |
C:/root/id.js |
/@fs/root/id.js(POSIX) |
去 /@fs/,保留 / |
/root/id.js |
file:///C:/root/id.js(Windows) |
去 file:/// |
C:/root/id.js |
file:///root/id.js(POSIX) |
去 file:/// 保留根 / |
/root/id.js |
| 多个前导斜杠 | 折叠为一个 | 统一形式 |
设计思想
- 执行状态与转换状态分离 :服务端图管"怎么生成代码",客户端图管"代码跑没跑、导出了什么",以
id一一对应。这样服务端可以无状态化(每次只回答"是否失效"),客户端成为执行状态的唯一权威 - 三张表各司其职 :
id用于执行与依赖分析,file用于热更新定位,url用于取数去重。缺少任何一张都会导致特定场景失效(重复执行 /HMR漏更新 / 重复fetch) - 幂等节点 +
URL别名 :ensureModule保证"同一模块多个访问路径"收敛为一个执行实例,这是ESM单实例语义的前提 - 执行前暴露
exports:exports由runner在evaluate前赋值,循环依赖因此能拿到部分命名空间,与原生ESM行为一致 - 失效是局部的、保留图结构 :只重置执行相关字段并清正向边,保留反向边以维持
HMR回溯能力,并明确接受一个罕见误报 - 惰性与缓存 :
sourcemap只在需要时解析并缓存;meta同时充当"是否已fetch"的标志位。 - 跨平台/跨运行时归一化 :
normalizeModuleId统一Windows盘符、/@fs/、node:、file://等差异,decodeBase64在没有Buffer的环境回退atob,使同一套运行时逻辑可用于Node与浏览器侧