EvaluatedModules

0 阅读7分钟

源码文件:evaluatedModules.ts

文件定位与整体设计

负责执行状态本身,它是 Module Runner 的运行时模块图 + 缓存,是 Module Runner 的状态层,用三张索引表维护模块执行图,并通过归一化、幂等创建、局部失效和懒解析,为 loader 提供一致、可缓存、可热更新的模块状态视图

服务端也有一个模块图(Vite 的 ModuleGraph),但它记录的是 transform 关系(谁 import 谁、转换结果、失效标记)。本文件记录的是运行状态:

  • 这个模块执行完了吗(evaluated)
  • 正在执行中的模块是什么(promise)
  • 导出对象是什么(exports)
  • 谁 import 了我、我 import 了谁(importers / imports)
  • 服务端给的元数据、内联 sourcemap 缓存(meta / map)

EvaluatedModuleNode

单个模块的运行状态节点

字段类型含义与作用
idstring(构造参数,只读)规范模块 id。通常是文件系统路径 + query/hash(如 C:/proj/src/a.vue?vue&type=script),也可能是虚拟模块 id(\0virtual:xxx)。它是与服务端模块图对应的键。
urlstring(构造参数,只读)import 语句中使用的原始 URL(未 resolve),如 /src/a.vue、/@fs/C:/...。用于 full-reload 时重新 import(url)。
importersSet<string>反向边:谁 import 了我(存 id)。HMR 的 full-reload 需要沿它回溯到入口;runner.cachedRequest 每次请求都会补充。
importsSet<string>正向边:我 import 了谁(存 id)。循环依赖检测(isCircularRequest)依赖它。
evaluatedboolean是否已完成执行。初始 false;cachedRequest 在 finally 中置 true;invalidateModule 重置。
metaResolvedResult | undefined服务端返回的元数据(externalize 信息或 code/file/id/url)。是否有 meta 是判断“模块信息已获取”的依据(isCached)。
promisePromise<any> | undefined执行中的 promise。用于并发去重和循环依赖协同:非 undefined 且未 evaluated 即正在执行。
exportsany模块命名空间对象。在代码执行前就赋值(directRequest 中),使循环依赖能拿到部分导出;执行完成后被 Object.seal。
filestringcleanUrl(id) 的结果,即文件路径(去掉 query/hash)。用于 fileToModulesMap 索引,供 HMR 按文件查找节点。
mapDecodedMap | undefined内联 sourcemap 的懒解析缓存。首次取堆栈映射时解析,之后复用。

设计要点

  • 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 中

表keyvalue服务的场景
idToModuleMap规范 id节点主索引:执行、循环检测、HMR 回溯
fileToModulesMap文件路径节点集合HMR 收到“某文件变了”时找出所有相关模块(一个文件可有多个 query 变体)
urlToIdModuleMapimport 的原始 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 与浏览器侧