PDF.js 源码解读: internal_evt.js
2026年10月27日 · StreamPDF Team

前言
本篇分析 src/shared/internal_evt.js,除去许可证与同步注释,其实现只有七行。web/internal_evt.js 中还有一个镜像文件。
这个模块不是事件总线,也没有定义任何事件名。它导出两个用于给事件监听器分类的值:
INTERNAL_EVT,在预处理阶段选定的 token;internalOpt,包含该 token 的冻结选项对象。
真正的注册与分发逻辑位于 web/event_utils.js。把两侧放在一起读,才能看到完整机制:内部监听器凭构建期 token 获得内部分类,而 EventBus 会先投递同一事件的内部监听器,再投递外部监听器。
模块总览
| 区块 | 行号 | 职责 |
|---|---|---|
| 同步注释 | 16-18 | 要求本文件与 web/internal_evt.js 保持一致 |
INTERNAL_EVT | 20-23 | 选择构建注入 token 或源码/测试回退值 |
internalOpt | 25 | 把 token 包装为不可变的监听器选项 |
| 导出 | 27 | 向 EventBus 提供 token,向内部调用方提供选项对象 |
这个设计清晰地区分了职责:本模块拥有能力值,内部组件通过 internalOpt 声明分类,EventBus 再用严格相等比较决定监听器类别。
INTERNAL_EVT:在构建期选择 token
const INTERNAL_EVT =
typeof PDFJSDev !== 'undefined' && !PDFJSDev.test('TESTING')
? PDFJSDev.eval('INTERNAL_EVT')
: 'internalEvent';这里有两种模式:
| 模式 | 选定的值 | 原因 |
|---|---|---|
| 预处理后的非测试构建 | PDFJSDev.eval("INTERNAL_EVT") | 注入构建定义提供的值 |
| 原始源码或测试构建 | "internalEvent" | 不依赖生产构建注入,提供确定性回退值 |
生产值来自 gulpfile.mjs 中的 DEFINES 对象:
const DEFINES = Object.freeze({
// Other build flags...
INTERNAL_EVT: crypto.randomUUID(),
});Babel 预处理器根据这些定义计算 PDFJSDev.eval,再把表达式替换为结果字面量。因此,发布模块不会在运行时调用 crypto.randomUUID();UUID 在构建产物生成时选定,并在该构建中保持固定。
这个 token 不是内部事件的名称,而是监听器选项中的一种能力式标识。调用方不能只传 { internal: true } 就变成内部监听器,因为 EventBus 会用严格相等比较检查传入值是否等于 INTERNAL_EVT。
固定的测试回退值服务于另一个目的。源码级测试与未预处理导入需要比较双方保持一致,但不能依赖生产预处理流程。它不应与非测试构建使用的随机值混为一谈。
internalOpt:冻结内部注册选项
const internalOpt = Object.freeze({ internal: INTERNAL_EVT });internalOpt 把 token 变成 EventBus.on 所期望的选项形状。冻结可以防止某个消费者修改共享对象的 internal 属性,进而悄悄影响之后的注册。这里的冻结是浅冻结,但对象只有一个原始值属性,因此已经足够。
内部调用方可以直接传入这个对象,也可以与其他标准选项组合。例如,批注编辑器 UI 管理器注册多个带中止信号的监听器:
const evtOpts = { signal, ...internalOpt };
eventBus.on('editingaction', this.onEditingAction.bind(this), evtOpts);
eventBus.on('pagechanging', this.onPageChanging.bind(this), evtOpts);
eventBus.on('scalechanging', this.onScaleChanging.bind(this), evtOpts);展开语法把 token 复制到新的选项对象,同时保留 signal。其他调用点也会用同样方式添加 once。这样,生命周期与投递分类可以自由组合,不必为每种组合发明单独的注册 API。
EventBus.on() 与 dispatch():落实内部优先投递
EventBus 记录监听器时,token 才开始发挥作用。在 web/event_utils.js 中,注册逻辑通过与模块契约值的严格比较,保存一个布尔分类:
this.#listeners.getOrInsertComputed(eventName, makeSet).add({
listener,
internal: options?.internal === INTERNAL_EVT,
once: options?.once === true,
rmAbort,
});因此,三种情况都会产生外部监听器:没有选项、没有 internal 属性,或者属性值不等于构建 token。只有匹配的 token 才会把保存的 internal 标记设为真。
分发时,EventBus 根据这个标记把同一事件分成两个阶段:
for (const { listener, internal, once } of new Set(eventListeners)) {
if (once) {
this.off(eventName, listener);
}
if (!internal) {
(extListeners ??= []).push(listener);
continue;
}
listener(data);
}
if (extListeners) {
for (const listener of extListeners) {
listener(data);
}
}内部回调在第一轮执行;外部回调先被收集,之后再执行。源码注释直接说明了原因:先让 viewer 组件处理事件并更新状态,再让外部观察者运行。
这里做的是监听器分类与排序,而不是两套独立总线。外部监听器仍然会收到事件。new Set(eventListeners) 创建的快照也能在投递期间增删监听器时保持本轮分发稳定,once 监听器则会在调用前移除。
在原始源码与 GENERIC 分支中,EventBus 构造函数还会调用 Object.seal(this)。源码注释明确把它与“防止第三方覆写方法后获取 INTERNAL_EVT”联系起来。对象密封、构建期随机 token 以及严格比较,共同支撑这套内部/外部契约。
镜像文件:让 library 与 viewer 遵守同一契约
src/shared/internal_evt.js 被 src/display/editor/tools.js 等显示层代码导入;viewer 模块和 web/event_utils.js 则导入 web/internal_evt.js。两个实现完全相同,并且都带有要求维护者保持同步的注释。
构建时,两侧都通过同一个构建定义项解析 INTERNAL_EVT,所以严格比较使用的是同一个 UUID。这意味着显示 library 与 viewer 之间不需要在运行时交换秘密值。对应关系由预处理建立,而重复的固定回退值让直接源码运行与测试也保持一致。
因此,这种重复是有意设计,但不代表两个文件可以独立演化。如果只修改一边的选项形状或回退值,分类所依赖的约定就会被破坏。
总结
这个小模块支撑的是一套精确的事件路由机制,而不是自带一套事件系统:
- 非测试构建通过
PDFJSDev.eval注入随机 UUID;原始源码与测试使用固定的internalEvent回退值。 INTERNAL_EVT是比较 token,不是事件名,{ internal: true }不能替代它。internalOpt把 token 封装为冻结、可复用的选项对象,并能与signal、once组合。EventBus.on把成功的身份比较转换为内部分类;dispatch先调用内部监听器,再调用外部监听器。- shared 与 web 两份文件必须保持同步,才能让显示层注册与 viewer 侧分发始终遵守同一契约。
关键认识是:构建期注入与事件排序是同一设计的两半。token 确定哪些监听器属于内部;分发器利用这一信息,在把事件交给外部观察者前先更新 viewer 状态。
StreamPDF 的设计原则: 能前置处理的绝对不放到解析的时候才做, 由服务端一次解决所有性能问题. 浏览器只获取&渲染必须数据.
相关文章
- PDF.js 从 demo 深入浅出 10: CanvasGraphics 与 operatorList 绘制2026年10月27日
- PDF.js 从 demo 深入浅出 9: 网络流与 Range 请求2026年10月27日
- PDF.js 从 demo 深入浅出 8: PDFWorker.create 与真假 Worker 握手2026年10月27日
- PDF.js 从 demo 深入浅出 7: WorkerTransport 初始化2026年10月27日
- PDF.js 从 demo 深入浅出 6: MessageHandler 跨线程通讯2026年10月27日
- PDF.js 从 demo 深入浅出 5: render 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 4: getViewport 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 3: getPage 方法全貌2026年10月27日
- PDF.js 从 demo 深入浅出 2: getDocument 方法全貌2026年10月27日
- PDF.js 源码解读: TextLayer2026年10月27日
- PDF.js 源码解读: font_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_display2026年10月27日
- PDF.js 源码解读: PDFObjects2026年10月27日
- PDF.js 源码解读: 网络流2026年10月27日
- PDF.js 源码解读: math_clamp.js2026年10月27日
- PDF.js 源码解读: css_utils.js2026年10月27日
- PDF.js 源码解读: scripting_utils2026年10月27日
- PDF.js 源码解读: MurmurHash3_642026年10月27日
- PDF.js 源码解读: obj_bin_transform_utils2026年10月27日
- PDF.js 源码解读: image_utils.js2026年10月27日
- PDF.js 源码解读: BasePDFStream2026年10月27日
- PDF.js 源码解读: 共享工具层2026年10月27日
- PDF.js 源码解读: MessageHandler2026年10月27日
- PDF.js 源码解读: api2026年10月27日
- PDF.js 源码解读: fonts2026年10月27日
- PDF.js 源码解读: glyphlist2026年10月27日
- PDF.js 源码解读: binary_cmap2026年10月27日
- PDF.js 源码解读: cmap2026年10月27日
- PDF.js 源码解读: encodings2026年10月27日
- PDF.js 源码解读: metrics2026年10月27日
- PDF.js 源码解读: standard_fonts2026年10月27日
- PDF.js 源码解读: fonts_utils2026年10月27日
- PDF.js 源码解读: default_appearance2026年10月27日
- PDF.js 源码解读: metadata_parser2026年10月27日
- PDF.js 源码解读: name_number_tree2026年10月27日
- PDF.js 源码解读: struct_tree2026年10月27日
- PDF.js 源码解读: catalog2026年10月27日
- PDF.js 源码解读: pdf_manager2026年10月27日
- PDF.js 源码解读: object_loader2026年10月27日
- PDF.js 源码解读: obj_bin_transform_core2026年10月27日
- PDF.js 源码解读: crypto2026年10月27日
- PDF.js 源码解读: core_utils2026年10月27日
- PDF.js 源码解读: primitives2026年10月27日
- PDF.js 源码解读: worker_stream2026年10月27日
- PDF.js 源码解读: ccitt_stream2026年10月27日
- PDF.js 源码解读: jpx_stream2026年10月27日
- PDF.js 源码解读: jbig2_stream2026年10月27日
- PDF.js 源码解读: jpeg_stream2026年10月27日
- PDF.js 源码解读: decrypt_stream2026年10月27日
- PDF.js 源码解读: brotli_stream2026年10月27日
- PDF.js 源码解读: run_length_stream2026年10月27日
- PDF.js 源码解读: ascii_hex_stream2026年10月27日
- PDF.js 源码解读: ascii_85_stream2026年10月27日
- PDF.js 源码解读: lzw_stream2026年10月27日
- PDF.js 源码解读: predictor_stream2026年10月27日
- PDF.js 源码解读: flate_stream2026年10月27日
- PDF.js 源码解读: decode_stream2026年10月27日
- PDF.js 源码解读: chunked_stream2026年10月27日
- PDF.js 源码解读: stream2026年10月27日
- PDF.js 源码解读: base_stream2026年10月27日
- PDF.js 深入浅出 11: Parser2026年10月26日
- PDF.js 深入浅出 10: XRef2026年10月26日
- PDF.js 深入浅出 9: CanvasGraphics2026年10月26日
- PDF.js 深入浅出 8: Document 与 Page2026年10月26日
- AI 时代, 我们到底应该做什么才能活下去(StreamPDF 创立有感)2026年10月26日
- PDF.js 深入浅出 7: PartialEvaluator2026年10月25日
- PDF.js 深入浅出 6: PDFManager.initDocument2026年10月25日
- PDF.js 深入浅出 5: PDFFetchStream 和 PDFWorkerStream2026年10月25日
- PDF.js 深入浅出 4: MessageHandler2026年10月22日
- PDF.js 深入浅出 3: getViewport & render2026年10月22日
- PDF.js 从 demo 深入浅出 1: 自定义 viewer 渲染 PDF2026年10月19日
- PDF.js 深入浅出 2: getDocument & getPage2026年10月19日
- PDF.js 深入浅出 1: PDF.js 的架构设计2026年10月16日