Puppeteer AwaitableIterable:统一同步与异步可迭代集合的类型设计与查询系统源码解析
AwaitableIterable<T> 是 Puppeteer 内部查询系统(QueryHandler 体系)中的核心类型抽象,定义为 Iterable<T> | AsyncIterable<T> 的联合类型。本篇以 API 文档中该类型的签名为起点,结合当前仓库源码,解析它为何存在、在 querySelectorAll、ARIA 查询、浏览器端句柄"转置"(transpose)等关键链路中承担什么角色,以及开发者在注册自定义查询处理器(custom query handler)时如何正确地使用同步 Iterable 或异步 AsyncIterable 作为返回值。
一、类型签名:一行定义背后的双重世界
文档 docs/api/puppeteer.awaitableiterable.md 给出的完整定义如下:
export type AwaitableIterable<T> = Iterable<T> | AsyncIterable<T>;
该签名在仓库中的权威定义位于 types.ts(第 56 行,标注为 @public):
/**
* @public
*/
export type AwaitableIterable<T> = Iterable<T> | AsyncIterable<T>;
它表达的是一个非常实际的约束:Puppeteer 的查询函数需要在同一个函数签名下,同时容纳两种本质不同的迭代协议——
Iterable<T>:同步可迭代集合(如数组、Set、同步 generator),调用Symbol.iterator后同步next();AsyncIterable<T>:异步可迭代集合(如AsyncGenerator),调用Symbol.asyncIterator后next()返回 Promise。
为什么必须兼容两者?因为 Puppeteer 的查询逻辑最终要在两个执行世界中运行:
- 浏览器内(injected 代码):此时 DOM 已就绪,查询天然可以是同步
Iterable<Node>,无需等待任何 IO; - Node.js 侧(Puppeteer 进程):结果需要跨 CDP/WebDriver BiDi 通道传输,每一步
next()都是一次网络往返,只能是AsyncIterable。
AwaitableIterable 正是让这两类实现可以共用同一接口、同一套消费代码的"桥梁类型"。
同族类型对照
在同一文件 types.ts 中,还有两个紧密相关的类型,理解它们有助于把握整体类型体系:
| 类型 | 定义 | 可见性 |
|---|---|---|
Awaitable<T> |
T | PromiseLike<T>(第 61 行) |
@public |
AwaitableIterator<T> |
Iterator<T> | AsyncIterator<T>(第 51 行) |
@internal |
AwaitableIterable<T> |
Iterable<T> | AsyncIterable<T>(第 56 行) |
@public |
其中 Awaitable<T> 是"值可能已就绪、也可能是 Promise"的泛化,与 AwaitableIterable 是同一思想在标量层面的对应;AwaitableIterator 则是迭代器(iterator)粒度的版本,主要服务于浏览器端句柄转置逻辑。AwaitableIterable 是三者中唯一出现在公共 API 表面、供外部扩展者(自定义查询处理器)直接面对的类型。
二、核心应用一:QuerySelectorAll 与 QueryHandler.queryAll
AwaitableIterable 最典型的落点是查询函数的返回类型。QueryHandler.ts 定义了内部类型别名(第 23 行附近):
export type QuerySelectorAll = (
node: Node,
selector: string,
PuppeteerUtil: PuppeteerUtil,
) => AwaitableIterable<Node>;
注意这里的语义:注入浏览器内的 querySelectorAll 实现被约束为返回 AwaitableIterable<Node>。对于纯 CSS 选择器,注入端可以直接返回 document.querySelectorAll(...)(NodeList,同步可迭代);而对于需要异步逻辑的选择器(如 XPath 或带轮询语义的自定义 handler),也可以返回 AsyncIterable。Puppeteer 外层统一按异步方式消费,调用方因此不需要区分底层是哪种实现。
对外暴露的 QueryHandler.queryAll(QueryHandler.ts 第 105-117 行)本身就是一个异步 generator:
static async *queryAll(
element: ElementHandle<Node>,
selector: string,
): AwaitableIterable<ElementHandle<Node>> {
using handle = await element.evaluateHandle(
this._querySelectorAll,
selector,
LazyArg.create(context => {
return context.puppeteerUtil;
}),
);
yield* transposeIterableHandle(handle);
}
它做两件事:
- 通过
evaluateHandle在页面内执行注入端的querySelectorAll,得到一个"指向浏览器内可迭代集合的句柄"(JSHandle<AwaitableIterable<Node>>); - 调用
transposeIterableHandle把浏览器内的迭代过程"转置"为 Node 侧逐元素拉取的AsyncIterable<ElementHandle<Node>>。
也就是说,AwaitableIterable<ElementHandle<Node>> 这个返回类型,精确刻画了"从远程浏览器中逐个(或分批)产出本地元素句柄"的行为。
三、核心应用二:HandleIterator——把浏览器端 Iterable 转置为本地句柄流
HandleIterator.ts 是 AwaitableIterable 类型在类型系统中被最重使用的模块。其入口函数签名为:
export async function* transposeIterableHandle<T>(
handle: JSHandle<AwaitableIterable<T>>,
): AsyncIterableIterator<HandleFor<T>>
参数 JSHandle<AwaitableIterable<T>> 的含义是:句柄所指向的浏览器内值本身必须是一个可迭代集合(同步或异步均可)。函数体先在浏览器内把该集合包装成 AsyncGenerator(yield* iterable,兼容两种协议),再分批拉取回 Node 侧:
const DEFAULT_BATCH_SIZE = 20;
async function* fastTransposeIteratorHandle<T>(
iterator: JSHandle<AwaitableIterator<T>>,
size: number,
) {
using array = await iterator.evaluateHandle(async (iterator, size) => {
const results = [];
while (results.length < size) {
const result = await iterator.next();
if (result.done) {
break;
}
results.push(result.value);
}
return results;
}, size);
// ... 将数组属性逐个转为 HandleFor<T> 并 yield,最后统一 dispose
}
async function* transposeIteratorHandle<T>(
iterator: JSHandle<AwaitableIterator<T>>,
) {
let size = DEFAULT_BATCH_SIZE;
while (!(yield* fastTransposeIteratorHandle(iterator, size))) {
size <<= 1; // 满批则批次翻倍:20 → 40 → 80 ...
}
}
这里有两个值得注意的实现细节:
- 批量转置 + 自适应批次:默认每次从浏览器拉 20 个元素(
DEFAULT_BATCH_SIZE = 20),若恰好取满则批次大小左移一位翻倍,显著降低大列表查询的 CDP 往返次数; - 自动资源回收:每批句柄通过
DisposableStack注册disposeSymbol回调(第 38-43 行),批次消费结束后统一释放,避免浏览器端对象泄漏。
从源码结构看,AwaitableIterable 在此处的价值在于:transposeIterableHandle 完全不需要知道浏览器内那个值究竟是 NodeList、Set 还是某个 AsyncGenerator——它只要求"可迭代",包装与拉取逻辑对两种协议透明。
四、核心应用三:AsyncIterableUtil——用 for await 统一消费两种集合
AsyncIterableUtil.ts 是仓库中所有工具函数的统一消费层,其每个方法的首参数都是 AwaitableIterable<T>:
export class AsyncIterableUtil {
static async *map<T, U>(
iterable: AwaitableIterable<T>,
map: (item: T) => Promise<U>,
): AsyncIterable<U> {
for await (const value of iterable) {
yield await map(value);
}
}
static async *flatMap<T, U>(
iterable: AwaitableIterable<T>,
map: (item: T) => AwaitableIterable<U>,
): AsyncIterable<U> { /* for await + yield* */ }
static async collect<T>(iterable: AwaitableIterable<T>): Promise<T[]> {
const result = [];
for await (const value of iterable) {
result.push(value);
}
return result;
}
static async first<T>(
iterable: AwaitableIterable<T>,
): Promise<T | undefined> { /* 取首个元素即返回 */ }
}
关键技巧是:for await...of 可以直接消费同步 Iterable(TypeScript/JS 规范保证异步迭代协议可以包裹同步迭代器),因此这组工具函数不需要任何 Symbol.iterator 与 Symbol.asyncIterator 的分支判断,就能同时处理两种来源。这解释了 AwaitableIterable 为何设计成"联合类型 + 异步消费"的形态——消费端永远按最慢的(异步)协议走,实现端则按各自最自然的方式返回。
五、核心应用四:ARIA 查询与 queryAXTree 的惰性产出
公共 API 中 AwaitableIterable 还出现在抽象方法 ElementHandle.queryAXTree 的签名里(api/ElementHandle.ts 第 1033-1036 行,标记为 @internal):
abstract queryAXTree(
name?: string,
role?: string,
): AwaitableIterable<ElementHandle<Node>>;
两个协议实现都遵循该契约:
- CDP 实现(cdp/ElementHandle.ts 第 180-208 行):先一次性向
Accessibility.queryAXTree请求整棵无障碍树,过滤ignored节点与NON_ELEMENT_NODE_ROLES后,通过AsyncIterableUtil.map(results, node => this.realm.adoptBackendNode(node.backendDOMNodeId))惰性地将每个节点转成本地ElementHandle——只有消费方真正next()时才发起adopt调用; - BiDi 实现(bidi/ElementHandle.ts 第 125 行)同样以
AwaitableIterable<ElementHandle<Node>>为返回类型。
ARIA 查询处理器 ARIAQueryHandler.ts 则展示了类型在继承体系中的传递:
static override async *queryAll(
element: ElementHandle<Node>,
selector: string,
): AwaitableIterable<ElementHandle<Node>> {
const {name, role} = parseARIASelector(selector);
yield* element.queryAXTree(name, role);
}
static override queryOne = async (
element: ElementHandle<Node>,
selector: string,
): Promise<ElementHandle<Node> | null> => {
return (
(await AsyncIterableUtil.first(this.queryAll(element, selector))) ?? null
);
};
queryOne 借助 AsyncIterableUtil.first 直接消费 queryAll 产出的 AwaitableIterable,取第一个匹配项——这正是"同步/异步可迭代统一消费"模式的典型小样本。
六、消费端闭环:page.$$ 如何落到 AwaitableIterable
上述类型最终通过 ElementHandle 的私有查询实现汇入公共 API $$(api/ElementHandle.ts 第 443-451 行):
async #$$impl<Selector extends string>(
selector: Selector,
): Promise<Array<ElementHandle<NodeFor<Selector>>>> {
const {updatedSelector, QueryHandler} =
getQueryHandlerAndSelector(selector);
return await (AsyncIterableUtil.collect(
QueryHandler.queryAll(this, updatedSelector),
) as Promise<Array<ElementHandle<NodeFor<Selector>>>>);
}
完整调用链为:element.$$ → QueryHandler.queryAll(返回 AwaitableIterable<ElementHandle<Node>>)→ AsyncIterableUtil.collect 将其物化为 ElementHandle[]。开发者在用户代码中拿到的数组,本质上就是一次对 AwaitableIterable 的完整异步消费。
七、实践指导:自定义查询处理器该如何返回
对于扩展 Puppeteer 的开发者,AwaitableIterable 通过 CustomQueryHandler 暴露到注册脚本一侧。注册接口对注入浏览器的 queryAll 要求返回 Iterable<Node>(同步),因为注入代码运行在页面内,DOM 是同步可达的:
export interface CustomQueryHandler {
queryOne?: (node: Node, selector: string) => Node | null;
queryAll?: (node: Node, selector: string) => Iterable<Node>;
}
使用方式(与文档注释一致):
import puppeteer from 'puppeteer';
puppeteer.registerCustomQueryHandler('lit', {
queryAll: (node, selector) => {
// 浏览器内同步执行,返回可迭代集合即可
return (node as HTMLElement).shadowRoot?.querySelectorAll(selector) ?? [];
},
});
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.setContent(`<div id="root"></div>`);
const handles = await page.$$('lit/…'); // 经 AwaitableIterable 链路产出
await browser.close();
结合源码可以得出两条实践结论:
- 注入端优先返回同步
Iterable:浏览器内没有 IO,同步集合可直接被transposeIterableHandle统一包装(yield* iterable),无需承担不必要的异步开销;AwaitableIterable的AsyncIterable分支主要是为 Node 侧的远程拉取阶段服务的。 - Node 侧扩展一律按
AsyncIterable对待:任何自行封装"产出ElementHandle序列"的逻辑,都应返回AsyncIterable/异步 generator,并复用 AsyncIterableUtil 的map/flatMap/collect/first来保持与核心代码一致的消费语义和惰性特性。
八、小结
| 维度 | 说明 |
|---|---|
| 定义位置 | types.ts 第 56 行,@public |
| 签名 | Iterable<T> | AsyncIterable<T> |
| 设计动机 | 让"浏览器内同步查询结果"与"Node 侧跨传输的惰性句柄流"共用同一接口 |
| 关键生产者 | QueryHandler.ts 的 queryAll、ElementHandle.queryAXTree(CDP/BiDi 双实现)、AriaQueryHandler.ts |
| 关键消费者 | HandleIterator.ts 的 transposeIterableHandle(批量 20、满批翻倍、自动 dispose)、AsyncIterableUtil.ts 的 map/flatMap/collect/first |
| 公共 API 落点 | ElementHandle.$$ / Page.$$ 经由 AsyncIterableUtil.collect 将 AwaitableIterable<ElementHandle<Node>> 物化为数组 |
AwaitableIterable 本身只是一个两词联合类型,但它是 Puppeteer 查询子系统"双世界"架构的类型契约:浏览器端按最自然的方式产出集合,Node 端按最稳妥的异步协议消费集合,而中间的批次化转置与资源回收由 transposeIterableHandle 统一兜底。理解它,就理解了 Puppeteer 中 $$、ARIA 查询与自定义查询处理器共享的同一条执行管道。
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