首页
/ Puppeteer Awaitable 类型详解:`T | PromiseLike<T>` 如何支撑整个 API 的同步/异步双形态

Puppeteer Awaitable 类型详解:`T | PromiseLike<T>` 如何支撑整个 API 的同步/异步双形态

2026-09-05 15:56:39作者:温玫谨Lighthearted

Puppeteer 中大量的 API(如 evaluatewaitForFunction、Locator 系列的谓词与映射器)都要求传入"可以是同步值、也可以是异步 Promise"的对象或函数,这一约定由核心类型 Awaitable<T> 统一表达。本文以官方 API 文档中的 Awaitable 类型为起点,结合 puppeteer-core 源码 逐层展开它的定义动机、同族的 Awaitable 类型家族,以及在 Locator、谓词、映射器中的真实使用方式,帮助你理解为什么 Puppeteer 的绝大多数"回调即异步"接口都能同时接受同步与异步实现。

一、类型签名:一行定义,两种形态

官方 API 文档对 Awaitable 的描述非常克制,仅给出签名(见 docs/api/puppeteer.awaitable.md):

export type Awaitable<T> = T | PromiseLike<T>;

该定义位于 puppeteer-core 的公共类型文件中,源码位置为 packages/puppeteer-core/src/common/types.ts

/**
 * @public
 */
export type Awaitable<T> = T | PromiseLike<T>;

从这行定义可以直接读出三个设计要点:

  1. 联合类型而非单一定义Awaitable<T>TPromiseLike<T> 的联合。也就是说,任何接受 Awaitable<T> 的 API,调用方既可以传一个已就绪的 T 值,也可以传一个尚未 resolve 的 Promise。API 的实现侧用 await 一次性消化两种形态——对普通值 await 是恒等操作,对 Promise 则是等待。
  2. 标记为 @public:源码中的 TSDoc 注释将其标注为公开 API,这意味着它是 TypeScript 用户在使用 Puppeteer 时会实际接触到的类型(它会被传递到 page.evaluatelocator 等公开签名的位置),因此与内部 @internal 类型在语义稳定性上有所区别。
  3. 使用 PromiseLike 而非 Promise:这是整个类型的关键选择,下一节展开。

二、为什么是 PromiseLike<T> 而不是 Promise<T>

ES 标准库中的 PromiseLike<T> 接口(lib.es5.d.ts 中定义)只要求对象拥有一个 then 方法:

interface PromiseLike<T> {
  then<TResult1 = T, TResult2 = never>(
    onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | null,
    onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null,
  ): (PromiseLike<TResult1 | TResult2>);
}

选择更弱的结构类型 PromiseLike 而非具体的 Promise 构造器,带来两个实际好处:

  • 结构兼容第三方 Promise 实现:Q、Bluebird 等第三方 Promise 库、以及各种测试环境中的 mock Promise,只要实现了 then 即可满足 Awaitable。对 Puppeteer 这类需要"接收用户代码返回值"的库来说,用户代码里返回什么类型的 Promise 都不该被类型系统拒绝。
  • 类型系统层面更宽松:原生 Promise 还携带 catchfinally、静态方法等成员;用 PromiseLike 做联合右支,意味着"只需要可被 await"这一最小契约,这正是 Awaitable 名称的语义——它描述的不是"是什么",而是"可被等待"。

需要注意的边界:PromiseLike 只约束了 then 的正向路径签名,await 一个 PromiseLike 的失败(reject)行为依然由运行时处理;类型上它保证的是"可组合、可 await",而不保证任何额外的静态能力。

三、Awaitable 类型家族:同一文件中的近亲类型

Awaitable 并不是孤立存在的。在 types.ts 中,与它同文件定义的一组类型构成了一个"可等待"家族,全部以 @public@internal 标注:

类型 签名 可见性 用途
Awaitable<T> T | PromiseLike<T> @public 值本身可以是同步值或 Promise
AwaitablePredicate<T> (value: T) => Awaitable<boolean> @public 谓词函数,判断逻辑可以异步
AwaitableIterable<T> Iterable<T> | AsyncIterable<T> @public 可迭代对象,同步/异步迭代器皆可
AwaitableIterator<T> Iterator<T> | AsyncIterator<T> @internal 可迭代器的迭代器层面,内部使用
EvaluateFunc<T> (...params: InnerParams<T>) => Awaitable<unknown> @public evaluate 传入的函数,返回值可异步
EvaluateFuncWith<V, T> (...params: [V, ...InnerParams<T>]) => Awaitable<unknown> @public 带首参(this 绑定)的 evaluate 函数

其中 AwaitablePredicate 直接建立在 Awaitable 之上:

/**
 * @public
 */
export type AwaitablePredicate<T> = (value: T) => Awaitable<boolean>;

这个组合表达了一个 Puppeteer 的常见模式:过滤/判断回调既可以是纯同步的 (value) => boolean,也可以是返回 Promise<boolean> 的异步函数(例如谓词内部要发起网络请求或调用 evaluateHandle)。实现侧只需对返回值做一次 await 即可归一。

EvaluateFuncEvaluateFuncWith(见 types.ts)则说明 Awaitable 覆盖了另一大类 API:page.evaluate / frame.evaluate 等接口接受用户函数,其返回值声明为 Awaitable<unknown>,即"你在页面上下文里写的函数可以是 async 的"。配合 InnerParams 对参数做 HandleOr 扁平化,构成了 Puppeteer 求值 API 的完整类型链路。

四、实战印证:Awaitable 在 Locator 与谓词中的落地

类型定义本身抽象,真正的价值要看它在核心功能中的调用关系。Locator API(Puppeteer 对元素等待与操作的响应式封装)是 Awaitable 最密集的消费方之一,相关文件为 packages/puppeteer-core/src/api/locators/locators.ts

4.1 page.locator(func):接受返回 Awaitable 的工厂函数

PageFrame 都暴露了以函数创建 Locator 的重载:

// Page.ts L1203 / Frame.ts L543
locator<Ret>(func: () => Awaitable<Ret>): Locator<Ret>;

Page.tsFrame.ts。这个重载背后的实现类是 FunctionLocatorlocators.ts):

export class FunctionLocator<T> extends Locator<T> {
  static create<Ret>(
    pageOrFrame: Page | Frame,
    func: () => Awaitable<Ret>,
  ): Locator<Ret> {
    return new FunctionLocator<Ret>(pageOrFrame, func).setTimeout(
      'getDefaultTimeout' in pageOrFrame
        ? pageOrFrame.getDefaultTimeout()
        : pageOrFrame.page().getDefaultTimeout(),
    );
  }
  // ...
  _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<T>> {
    const signal = options?.signal;
    return defer(() => {
      return from(
        this.#pageOrFrame.waitForFunction(this.#func, {
          timeout: this.timeout,
          signal,
        }),
      );
    }).pipe(throwIfEmpty());
  }
}

可以看到 Awaitable 在这里完成了一次"类型到行为的映射":func: () => Awaitable<Ret> 这个回调被原样传给 waitForFunction,由后者负责轮询与等待。也就是说,Awaitable 类型约定(返回值可同步可异步)与 waitForFunction 的运行时行为(对每次求值结果做判断与轮询)是配套的——类型允许异步,运行时才提供轮询兜底。

4.2 谓词(Predicate):同步类型守卫与异步布尔的并集

Locator.filter 接受的谓词类型定义为(locators.ts):

export type Predicate<From, To extends From = From> =
  ((value: From) => value is To) | ((value: From) => Awaitable<boolean>);

export type HandlePredicate<From, To extends From = From> =
  | ((value: HandleFor<From>, signal?: AbortSignal) => value is HandleFor<To>)
  | ((value: HandleFor<From>, signal?: AbortSignal) => Awaitable<boolean>);

这是 Awaitable 参与类型收窄的典型例子:联合的左支是 TS 类型守卫(value is To,用于 filter 后把 Locator<From> 收窄为 Locator<To>),右支是返回 Awaitable<boolean> 的普通函数。由于 TS 的判别联合规则,只要谓词声明为类型守卫形式,filter 就能获得类型收窄能力;声明为异步形式则保留完整元素类型,允许回调内部执行异步逻辑(如检查网络请求、调用 evaluateHandle 等)。

4.3 映射器(Mapper):Promise.resolve 是 Awaitable 的运行时归一

Locator.map 的映射函数类型同样建立在 Awaitable 之上(locators.ts):

export type Mapper<From, To> = (value: From) => Awaitable<To>;

export type HandleMapper<From, To> = (
  value: HandleFor<From>,
  signal?: AbortSignal,
) => Awaitable<HandleFor<To>>;

其运行时消费点在 MappedLocator._wait 中(locators.ts):

override _wait(options?: Readonly<ActionOptions>): Observable<HandleFor<To>> {
  return this.delegate._wait(options).pipe(
    mergeMap(handle => {
      return from(Promise.resolve(this.#mapper(handle, options?.signal)));
    }),
  );
}

Promise.resolve(x) 是处理 Awaitable<T> 的标准手法:若 mapper 同步返回 HandleFor<To>Promise.resolve 原样包装;若返回 Promise,则直接复用该 Promise。外层再经 rxjs 的 from 转回 Observable 管道。这段代码直观展示了 Awaitable 约定的落地范式——类型层允许两种形态,实现层用一次 Promise.resolve/await 归一,调用者因此获得"写同步回调和写异步回调完全等价"的体验。

五、对使用者的实际含义

结合上述源码,Awaitable 给最终用户带来的规则可以归纳为三条:

  1. 凡是类型签名里出现 Awaitable<T> 的回调/返回值,同步与异步写法等价。 例如 page.locator(async () => await this.page.waitForResponse(...)) 与返回现成值的同步写法都合法;filter 的谓词里也可以 await 一个网络请求。
  2. 返回 async 函数不要求"必须可轮询":像 page.locator(func) 这种基于 FunctionLocator 的实现,func 最终交给 waitForFunction 轮询(见 4.1 的 _wait 实现),受 Locator 自身 timeout(默认取页面 defaultTimeout)约束;而 evaluate 类 API 中的 async 函数则是单次执行后 await 结果,两者等待语义不同,选型时应注意区分。
  3. 传递第三方 Promise 实现无需断言:由于右支是 PromiseLike,非原生但实现了 then 的 Promise 对象可以直接作为 Awaitable 的取值传入,无需 as unknown as Promise<T> 之类的类型断言。

六、延伸阅读与参考路径

总结来说,Awaitable<T> = T | PromiseLike<T> 虽只有一行,却是 Puppeteer 类型体系里"同步/异步双形态 API"的统一基石:向上,它派生出 AwaitablePredicateAwaitableIterableEvaluateFunc 等一批公开类型;向下,它在 Locator 谓词、映射器和 waitForFunction 的调用链中得到一致的运行时归一处理。理解这一类型,基本就理解了 Puppeteer API 中"为什么这里能写 async、那里也能不写"的全部原因。

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