Deno runtime/js 深度解析:运行时 JavaScript 层的加载顺序、Web API 实现与快照机制
本文为 Deno 运行时 runtime/js 目录的导读与实现剖析。读完你将掌握:这个目录中每个带数字前缀的 JavaScript/TypeScript 文件的职责与加载时机、Deno 内置 Web API(Blob、fetch、EventTarget、timers、URL、Worker 等)背后的 ops 调用链,以及这些 JS 源码如何被注册进扩展、编译进 V8 snapshot 并决定最终二进制中哪些代码"常驻、哪些按需加载"。
1. 目录定位:Deno 运行时 JS 层是什么
runtime/js 目录存放的是 Deno 运行时自身用纯 JavaScript(个别为 TypeScript)编写的核心代码。目录 README(runtime/js/README.md)给出的三句话定位是:
- 这里存放 Deno runtime code written in plain JavaScript;
- 每个文件都是一个 ES module,文件名以数字开头,数字告诉 Deno 这些脚本应以什么顺序加载进 V8 isolate;
- 该目录(及整个
ext/体系)承载了 Deno 中可用的 Web APIs,其中部分实现可能与规范不完全对齐,且不少 Web API 底层依赖 ops,例如console、performance。
从源码结构看,"数字前缀决定加载顺序"这一约定在构建链路上落到了两个具体位置:扩展注册表 runtime/shared.rs 中按编号列出的 esm 与 lazy_loaded_js 清单,以及快照构建入口 runtime/snapshot.rs 中有严格注释要求保持顺序一致的扩展数组(注释明确写着 NOTE(bartlomieju): ordering is important here, keep it in sync with runtime/worker.rs, runtime/web_worker.rs, runtime/snapshot_info.rs and runtime/snapshot.rs!)。
2. 文件清单:14 个文件各自的职责
当前目录共 14 个源码文件(另有本 README),全部在 runtime/shared.rs 的 extension!(runtime, ...) 声明中被引用。按编号分组如下:
2.1 早期编号(01–41):lazy 加载的基础设施模块
这 8 个文件被声明为 lazy_loaded_js——不占用启动热路径,运行时通过 core.loadExtScript() 按需加载:
| 文件 | 职责 | 依赖的 ops / 关键实现 |
|---|---|---|
| 01_errors.js | 定义 NotFound、ConnectionRefused 等 Deno 特有错误类(基于 core 提供的 BadResource、Interrupted、NotCapable) |
纯 JS 类定义,配合 primordials |
| 01_version.ts | Deno.version 的数据载体,setVersions() 写入 deno / v8 / typescript 三个版本号 |
由 bootstrap 阶段填充 |
| 06_util.js | 日志级别等工具逻辑;文件内注释 WARNING: Keep this in sync with Rust (search for LogLevel),LogLevel 枚举(Error=1…Debug=4)需与 Rust 侧 LogLevel 保持同步 |
op_bootstrap_log_level |
| 10_permissions.js | Deno.permissions(query/request/revoke)的 JS 侧实现 |
op_query_permission、op_request_permission、op_revoke_permission |
| 11_workers.js | Worker 的宿主侧消息通道与生命周期 |
op_create_worker、op_host_post_message、op_host_post_message_raw、op_host_recv_ctrl、op_host_recv_message、op_host_recv_message_sync、op_host_terminate_worker |
| 40_fs_events.js | Deno.watchFs 的异步迭代器实现(支持 SymbolAsyncIterator、SymbolDispose) |
op_fs_events_open、op_fs_events_poll |
| 40_tty.js | 控制台尺寸查询(consoleSize() 写入 Uint32Array(2)) |
op_console_size |
| 41_prompt.js | alert / confirm / prompt 对话框;prompt 从 core.loadExtScript("ext:deno_io/12_io.js") 拿 stdin,在非 TTY 下直接返回 |
op_read_line_prompt |
注意 01_version.ts 是唯一以 TypeScript 编写的文件——它会被 transpile feature 引入的 deno_ast 在快照构建前转译(见 runtime/Cargo.toml 中 snapshot = ["transpile"]、transpile = ["deno_ast"])。
2.2 后期编号(90–99):ESM 清单与全局作用域
| 文件 | 职责 |
|---|---|
| 90_deno_ns.js | Deno 命名空间本体的 ESM 入口。它用 core.loadExtScript() 聚合 02_timers.js、22_http_client.js、01_console.js、00_ffi.js、01_net.js 等模块,并从 ext:core/ops 引入 op_net_listen_udp、op_net_listen_unixpacket、op_runtime_cpu_usage、op_runtime_memory_usage 等 ops |
| 97_navigator_user_agent_data.js | navigator.userAgentData(User-Agent Client Hints API)实现,window 与 worker 作用域共享,依赖 op_bootstrap_user_agent |
| 98_global_scope_shared.js | window/worker 共用的全局安装逻辑:装载 02_event.js、01_console.js、11_workers.js 等;文件内注释解释了 console 为何要急切安装——它是"thin cppgc port",让全局 console 保持可写且身份稳定,这样 --inspect 的 wrapConsole 能 patch 到同一个对象,99_main.js 的 serve-worker 路径也能给 console 重加 per-worker 日志前缀 |
| 98_global_scope_window.js | main thread(window-like)作用域的额外全局,读取 op_bootstrap_language、op_bootstrap_numcpus、op_bootstrap_user_agent |
| 98_global_scope_worker.js | worker 作用域对应的版本,bootstrap ops 与 window 版对称 |
| 99_main.js | 快照的 ESM 入口点(全文件 1400+ 行),负责剥离非标准 API(如 delete Intl.v8BreakIterator)、装载各扩展脚本、定义 main() 主引导流程 |
3. Web API 实现清单与 ops 机制
README 的 "Implemented Web APIs" 一节列出了本运行时覆盖的 Web API。结合各扩展 crate 的源码位置,可整理成如下对应关系(这些 JS 实现分布在 ext/ 下各扩展 crate,runtime/js 负责组装它们):
-
Blob——表示不透明二进制数据;
-
Console——日志用途。README 特别指出
console、performance这类 API 底层使用 ops:源码印证了这一点,90_deno_ns.js 加载的ext:deno_web/01_console.js会调用op_internal_log等 op 把输出送到 Rust 侧终端处理; -
CustomEvent / EventTarget / EventListener——事件机制。README 的实现备注:Deno 没有 DOM 层级,因此没有可供事件 bubble/capture 的树,
EventTarget的冒泡/捕获语义与浏览器不同; -
fetch / Request / Response / Body / Headers——基于 Promise 的现代 HTTP 请求 API;
-
location / Location——ext/web/12_location.js 提供,
99_main.js启动时即通过core.loadExtScript("ext:deno_web/12_location.js")装载; -
FormData——
multipart/form-data序列化访问; -
Performance——高精度时间获取;
-
setTimeout / setInterval / clearTimeout / clearInterval——回调调度,实现在
ext:deno_web/02_timers.js; -
Streams——数据流的创建、组合与消费(web-streams polyfill);
-
URL / URLSearchParams——URL 构造与解析;
-
Worker——在独立线程中执行代码。README 的两条实现备注值得注意:
- 不支持 Blob URL;
- 无法转移对象所有权,
postMessage传递的数据被序列化为 JSON,而非按 structured cloning 算法克隆。
这与 11_workers.js 中导入的
op_host_post_message(JSON 路径)与op_host_post_message_raw(raw 路径,供内部消息使用)两个 op 的分工相吻合;跨线程消息的接收侧则对应 99_main.js 中导入的op_worker_recv_message/op_worker_recv_message_sync。
README 同时提醒:部分实现可能与规范不完全对齐,这在对齐测试(如 WPT)时是需要留意的边界。
4. 加载机制:esm 与 lazy_loaded_js 在源码中的注册
runtime/shared.rs 是整个 runtime 扩展的注册表,也是理解"数字前缀顺序"的钥匙:
extension!(runtime,
deps = [ deno_webidl, deno_tls, deno_web, deno_fetch, /* ... */ ],
esm_entry_point = "ext:runtime/90_deno_ns.js",
esm = [
dir "js",
"90_deno_ns.js",
"97_navigator_user_agent_data.js",
"98_global_scope_shared.js",
"98_global_scope_window.js",
"98_global_scope_worker.js"
],
lazy_loaded_js = [
dir "js",
"01_errors.js",
"01_version.ts",
"06_util.js",
"10_permissions.js",
"11_workers.js",
"40_fs_events.js",
"40_tty.js",
"41_prompt.js",
],
customizer = |ext: &mut Extension| {
#[cfg(not(feature = "exclude_runtime_main_js"))]
{
// 99_main.js is the snapshot's ESM entry point; its source is loaded
// from disk only during snapshot creation, so we don't duplicate it
// in the final binary's `.rodata`.
ext.esm_files.to_mut().push(ExtensionFileSource::loaded_during_snapshot(
"ext:runtime_main/js/99_main.js",
concat!(env!("CARGO_MANIFEST_DIR"), "/js/99_main.js"),
));
ext.esm_entry_point = Some("ext:runtime_main/js/99_main.js");
}
}
);
这段声明回答了三个关键问题:
- 哪些文件常驻:
esm清单(90–98 段)随扩展立即编译进 isolate; - 哪些文件按需加载:
lazy_loaded_js清单(01–41 段)在快照中登记但由core.loadExtScript("ext:runtime/xx.js")在实际用到时才求值; - 入口点是谁:默认情况下
99_main.js被标记为loaded_during_snapshot——源码只在快照创建时从磁盘读入,不复制进最终二进制的.rodata,从而避免同一段代码在二进制里出现两份。入口点常量PATH_FOR_99_MAIN_JS定义在 runtime/js.rs。
lazy_loaded_js / lazy_loaded_esm 两种形态的语义在 runtime/snapshot.rs 的 LazyExtensionFileKind 枚举注释中写得很清楚:前者按需经 core.loadExtScript() 加载,后者经 op_lazy_load_esm(即 Deno.core 暴露的 createLazyLoader 工厂)加载。
4.1 按需加载(lazy)在 99_main.js 中的实例
99_main.js 展示了两处精心设计的懒加载,是"启动性能"优化的典型案例:
// Deno.serve (00_serve.ts) chains through 23_request/23_response/22_body
// into the 208 KB web-streams polyfill. Only loaded if `deno serve` / a
// declarative server export is actually used.
let _serveMod;
const lazyServeMod = () =>
_serveMod ?? (_serveMod = core.loadExtScript("ext:deno_http/00_serve.ts"));
// ...
// 26_fetch pulls 22_body -> 06_streams (208 KB). The only thing 99_main
// needs from it at bootstrap is the wasm-streaming callback registration.
let _fetchMod;
const lazyFetchMod = () =>
_fetchMod ?? (_fetchMod = core.loadExtScript("ext:deno_fetch/26_fetch.js"));
即:deno serve 与 WebAssembly streaming 各自背后拖着一个约 208 KB 的 web-streams polyfill,启动引导期只有真正用到时才触发加载。这也解释了 README 所说的"数字前缀 = 加载顺序"在懒加载文件上的延伸含义:编号既是静态登记顺序,也约束了模块间的前置依赖。
4.2 快照构建与扩展顺序
create_runtime_snapshot() 按固定顺序把 telemetry、webidl、web、webgpu、fetch、crypto、ffi、net、tls、kv、cron、napi、http、io、fs、os、process、node 系列、deno_runtime(即本目录所属扩展)、worker host、fs events、permissions、tty、http runtime、bootstrap、runtime 等扩展喂给 V8 生成快照。该函数的返回结构 CreateRuntimeSnapshotOutput 会区分"已编入快照 blob 的 lazy specifier"与"仍需 include_str! 嵌入二进制的残余集合"——换句话说,runtime/js 中每个 lazy 文件最终是住在快照里还是二进制 .rodata 里,由这套机制在构建期自动裁定。
5. 相关 feature 开关
runtime/Cargo.toml 声明了若干与"如何提供这些 JS"直接相关的 cargo feature:
snapshot = ["transpile"]:启用快照生成(需要先把01_version.ts等 TS 转译成 JS);transpile = ["deno_ast"]:引入转译依赖;exclude_runtime_main_js:从导出的扩展中排除./js/99_main.js,供嵌入式用户自带入口的场景;hmr = ["transpile"]:开发特性,禁用快照的创建与加载,改为运行时直接从磁盘读取各扩展的lazy_loaded_*源码(即按LoadedFromFsDuringSnapshot路径读取),方便热改 JS。
6. 小结:如何定位和阅读这些文件
- 想知道某个
Deno.*API 或全局对象的 JS 侧落点:先看 90_deno_ns.js(Deno命名空间)与 98_global_scope_*.js(全局安装); - 想知道它依赖哪个 op:直接看文件顶部的
core.ops/ext:core/ops导入,op 名(op_*)即可在runtime/ops/与各ext/*/crate 中反查 Rust 实现; - 想知道文件何时被加载:查 runtime/shared.rs 的
esm/lazy_loaded_js归属,以及 99_main.js 中core.loadExtScript()的调用点; - 想知道启动全貌:从 99_main.js 的
main()引导流程入手,再对照 runtime/snapshot.rs 的扩展顺序注释。
需要说明的适用前提:以上分析基于当前仓库版本(deno_runtime v0.266.0,见 runtime/Cargo.toml);README 中"Web API 可能与规范不完全对齐"、"无 DOM 事件树"、"Worker postMessage 走 JSON 序列化"等实现备注,是理解 Deno 与浏览器行为差异时的一手依据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00