EvaluatedModules

源码文件:evaluatedModules.ts

文件定位与整体设计

负责执行状态本身 ,它是 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

方法解析

  1. getModuleById:最简单的按 id 查找。调用方:cachedRequest(取 meta)、isCircularRequest、hmrHandler(沿 importers 回溯)、sourcemap 拦截器
  2. getModulesByFile:按文件路径取所有 节点。核心调用方是 hmrHandler.ts,full-reload 时根据 triggeredBy 文件找到相关模块,再沿 importers 递归找入口。"文件 → 多模块"的映射是热更新正确性的基础(否则 a.vue?vue&type=script 更新了,a.vue 主体却收不到)
  3. getModuleByUrl:按请求 URL 获取模块。浏览器/客户端拿到的 URL 可能是 /@id/virtual:foo 或 /@id/__x00__foo,而 ensureModule 建节点时用的 url 是服务端返回的规范形式。unwrapId 去掉 /@id/ 前缀并把 __x00__ 还原成 \0,保证查得到
  4. ensureModule:getModuleInformation 每次 fetch 成功后调用,有则复用、无则创建的幂等语义 ,让任意层级的 import(包括并发、循环、重复)最终都收敛到同一节点
  • 先归一化 id :保证 node:fs、/@fs/...、file:///... 等形式不会产生重复节点
  • 已存在则复用,并为新 URL 注册别名 :同一个模块用不同 URL 到达时,url→node 表会同时存在多个键,这正是不重复执行的关键
  • 不存在则创建,并写入三张表 :文件索引用 node.file(已 clean)作为 key,把同一文件的多个变体聚到同一个 Set 中
  1. invalidateModule:重置执行状态,但保留图结构 。用于服务端 fetchModule 返回 invalidate: true(服务端判定客户端缓存已过期)和其他需要强制重新执行的场景
  • 清 promise/evaluated/exports → 下次请求会重新执行
  • 清 meta/map → 下次请求会重新 fetch、重新解析 sourcemap
  • 清 imports → 依赖关系可能已变化,重新执行时重建
  • 保留 importers :若清掉反向边,模块在重新执行装入图之前就是"孤立节点"。此时若文件被删除,HMR 沿 importers 找不到任何入口,full-reload 静默失效。保留旧边是有意的权衡 ,宁可偶尔多触发一次 reload(文件已删除但旧 importer 仍在),也不能漏掉真实更新
  1. 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)
  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
多个前导斜杠 折叠为一个 统一形式

设计思想

  1. 执行状态与转换状态分离 :服务端图管"怎么生成代码",客户端图管"代码跑没跑、导出了什么",以 id 一一对应。这样服务端可以无状态化(每次只回答"是否失效"),客户端成为执行状态的唯一权威
  2. 三张表各司其职 :id 用于执行与依赖分析,file 用于热更新定位,url 用于取数去重。缺少任何一张都会导致特定场景失效(重复执行 / HMR 漏更新 / 重复 fetch)
  3. 幂等节点 + URL 别名 :ensureModule 保证"同一模块多个访问路径"收敛为一个执行实例,这是 ESM 单实例语义的前提
  4. 执行前暴露 exports :exports 由 runner 在 evaluate 前赋值,循环依赖因此能拿到部分命名空间,与原生 ESM 行为一致
  5. 失效是局部的、保留图结构 :只重置执行相关字段并清正向边,保留反向边以维持 HMR 回溯能力,并明确接受一个罕见误报
  6. 惰性与缓存 :sourcemap 只在需要时解析并缓存;meta 同时充当"是否已 fetch"的标志位。
  7. 跨平台/跨运行时归一化 :normalizeModuleId 统一 Windows 盘符、/@fs/、node:、file:// 等差异,decodeBase64 在没有 Buffer 的环境回退 atob,使同一套运行时逻辑可用于 Node 与浏览器侧
相关推荐
定栓6 天前
Webpack迁到Vite第十节:vite.config.js 开发与构建流转全链路
vue.js·webpack·vite
定栓6 天前
Webpack迁到Vite第二节:JSX 到 Render 函数
vue.js·webpack·vite
NetX行者9 天前
用 Vite + Vue3 + UnoCSS 搭建企业级项目模板(一):基础骨架
vue·vite·unocss
孟陬10 天前
不用盲调性能参数 Vitest 5.0 新推出 `vitest doctor` 一键诊断测试慢卡点,自动给出最优提速配置
性能优化·单元测试·vite
cindershade12 天前
大包不是一个数字:Vite 构建体积的责任归因与边界治理
vite
cindershade19 天前
首屏不是 dist:用“加载账本”治理 Vite 的大包
性能优化·vite
陪我一起学编程21 天前
vue3笔记
前端·webpack·vue3·vite·脚手架·vue3笔记·组件式