首页
/ Deno runtime/js 深度解析:运行时 JavaScript 层的加载顺序、Web API 实现与快照机制

Deno runtime/js 深度解析:运行时 JavaScript 层的加载顺序、Web API 实现与快照机制

2026-09-06 10:56:30作者:何将鹤

本文为 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,例如 consoleperformance

从源码结构看,"数字前缀决定加载顺序"这一约定在构建链路上落到了两个具体位置:扩展注册表 runtime/shared.rs 中按编号列出的 esmlazy_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.rsextension!(runtime, ...) 声明中被引用。按编号分组如下:

2.1 早期编号(01–41):lazy 加载的基础设施模块

这 8 个文件被声明为 lazy_loaded_js——不占用启动热路径,运行时通过 core.loadExtScript() 按需加载:

文件 职责 依赖的 ops / 关键实现
01_errors.js 定义 NotFoundConnectionRefused 等 Deno 特有错误类(基于 core 提供的 BadResourceInterruptedNotCapable 纯 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_permissionop_request_permissionop_revoke_permission
11_workers.js Worker 的宿主侧消息通道与生命周期 op_create_workerop_host_post_messageop_host_post_message_rawop_host_recv_ctrlop_host_recv_messageop_host_recv_message_syncop_host_terminate_worker
40_fs_events.js Deno.watchFs 的异步迭代器实现(支持 SymbolAsyncIteratorSymbolDispose op_fs_events_openop_fs_events_poll
40_tty.js 控制台尺寸查询(consoleSize() 写入 Uint32Array(2) op_console_size
41_prompt.js alert / confirm / prompt 对话框;promptcore.loadExtScript("ext:deno_io/12_io.js")stdin,在非 TTY 下直接返回 op_read_line_prompt

注意 01_version.ts 是唯一以 TypeScript 编写的文件——它会被 transpile feature 引入的 deno_ast 在快照构建前转译(见 runtime/Cargo.tomlsnapshot = ["transpile"]transpile = ["deno_ast"])。

2.2 后期编号(90–99):ESM 清单与全局作用域

文件 职责
90_deno_ns.js Deno 命名空间本体的 ESM 入口。它用 core.loadExtScript() 聚合 02_timers.js22_http_client.js01_console.js00_ffi.js01_net.js 等模块,并从 ext:core/ops 引入 op_net_listen_udpop_net_listen_unixpacketop_runtime_cpu_usageop_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.js01_console.js11_workers.js 等;文件内注释解释了 console 为何要急切安装——它是"thin cppgc port",让全局 console 保持可写且身份稳定,这样 --inspectwrapConsole 能 patch 到同一个对象,99_main.js 的 serve-worker 路径也能给 console 重加 per-worker 日志前缀
98_global_scope_window.js main thread(window-like)作用域的额外全局,读取 op_bootstrap_languageop_bootstrap_numcpusop_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 特别指出 consoleperformance 这类 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 的两条实现备注值得注意:

    1. 不支持 Blob URL
    2. 无法转移对象所有权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. 加载机制:esmlazy_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");
    }
  }
);

这段声明回答了三个关键问题:

  1. 哪些文件常驻esm 清单(90–98 段)随扩展立即编译进 isolate;
  2. 哪些文件按需加载lazy_loaded_js 清单(01–41 段)在快照中登记但由 core.loadExtScript("ext:runtime/xx.js") 在实际用到时才求值;
  3. 入口点是谁:默认情况下 99_main.js 被标记为 loaded_during_snapshot——源码只在快照创建时从磁盘读入,不复制进最终二进制的 .rodata,从而避免同一段代码在二进制里出现两份。入口点常量 PATH_FOR_99_MAIN_JS 定义在 runtime/js.rs

lazy_loaded_js / lazy_loaded_esm 两种形态的语义在 runtime/snapshot.rsLazyExtensionFileKind 枚举注释中写得很清楚:前者按需经 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.jsDeno 命名空间)与 98_global_scope_*.js(全局安装);
  • 想知道它依赖哪个 op:直接看文件顶部的 core.ops / ext:core/ops 导入,op 名(op_*)即可在 runtime/ops/ 与各 ext/*/ crate 中反查 Rust 实现;
  • 想知道文件何时被加载:查 runtime/shared.rsesm / lazy_loaded_js 归属,以及 99_main.jscore.loadExtScript() 的调用点;
  • 想知道启动全貌:从 99_main.jsmain() 引导流程入手,再对照 runtime/snapshot.rs 的扩展顺序注释。

需要说明的适用前提:以上分析基于当前仓库版本(deno_runtime v0.266.0,见 runtime/Cargo.toml);README 中"Web API 可能与规范不完全对齐"、"无 DOM 事件树"、"Worker postMessage 走 JSON 序列化"等实现备注,是理解 Deno 与浏览器行为差异时的一手依据。

登录后查看全文
热门项目推荐
相关项目推荐