Grafast 标准步骤 `first`:从数组与迭代器中取首元素的高性能实现
2026/9/23 4:39:12 网站建设 项目流程

Grafast 标准步骤first:从数组与迭代器中取首元素的高性能实现

【免费下载链接】crystal🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal

导读

first是 Grafast(Graphile Crystal 仓库中的核心执行引擎)提供的标准步骤(standard step),它的职责非常简单:取得某个列表步骤(list plan)所产出数组中的第一项。本文围绕grafast/website/grafast/standard-steps/first.md展开,结合grafast/grafast/src/steps/first.ts的完整实现,讲解first的两种调用形态(数组优化路径与通用迭代器路径)、它的类型约束、底层执行语义,以及它在dataplan-pgConnectionStep等真实模块中的落地用法。读完后你将能准确判断何时该用first、何时应显式传false,并理解它在计划优化(optimize)阶段如何被化简为直接依赖。

一、first的作用与两种调用形态

按官方文档定义,first步骤会“产出给定步骤所产出数组中的第一项”(Yields the first entry in the array the given step yields)。核心 API 在 grafast/grafast/src/steps/first.ts 中导出:

export function first<TData>( plan: StepRepresentingList<TData>, array = true, ): FirstStep<TData> { return plan.operationPlan.cacheStep( plan, "GrafastInternal:first()", array, () => new FirstStep(plan, array), ); }

1.1 默认形态:数组优化(array = true

文档给出的最典型用法是:

const $firstItem = first($array);

当第二个参数省略(默认true)时,Grafast 认为传入的步骤代表的是一个数组(或null/undefined),从而启用更激进的优化路径——见下文unbatchedExecuteoptimalExecute两条快路径。

1.2 通用形态:迭代器/异步迭代器(array = false

文档明确指出:如果传入的参数是(异步)迭代器,需要显式传false以关闭数组优化:

// If the argument is an iterable, pass `false` to opt out of the array // optimizations const $firstItem = first($iterable, false);

这背后的原因在构造函数(first.ts)中一目了然:

constructor(parentPlan: StepRepresentingList<TData>, isArray = true) { super(); this.addStrongDependency(itemsOrStep(parentPlan)); if (isArray) { this.unbatchedExecute = unbatchedExecute; this.execute = optimalExecute<TData>; this.isSyncAndSafe = true; } else { this.isSyncAndSafe = false; } }
  • isArray = true:直接替换为极简的同步执行器,并把isSyncAndSafe置为true,允许 Grafast 在同步上下文安全复用结果。
  • isArray = false:放弃同步安全保证,执行期需要真正遍历迭代器才能取出首项。

二、类型约束:StepRepresentingList

first的第一个参数类型是StepRepresentingList<TData>,定义于 grafast/grafast/src/steps/connection.ts:

export type StepRepresentingList< TItem, TNodeStep extends Step = Step<TItem>, TEdgeStep extends EdgeCapableStep<TItem, TNodeStep> = EdgeStep<TItem, TNodeStep>, TCursorValue = string, > = | ConnectionOptimizedStep<TItem, TNodeStep, TEdgeStep, TCursorValue> | StepWithItems<TItem> | Step<Maybe<readonly TItem[]>>;

也就是说,first不仅接受普通的“产出数组的步骤”,也接受:

  • 经过游标优化(cursor-optimized)的连接步骤(ConnectionOptimizedStep);
  • 带有items()访问器的步骤(StepWithItems,即可以显式取到元素列表的步骤);
  • 直接产出readonly TItem[](允许为null/undefined)的步骤。

构造函数内部通过itemsOrStep(parentPlan)(定义于同一文件的 connection.ts)把“连接类步骤”统一归一为“元素列表步骤”,再建立强依赖(addStrongDependency),确保上游列表先于first执行完毕。

三、执行语义:数组快路径与迭代器兜底

FirstStep的执行逻辑分两层(first.ts 与 first.ts)。

3.1 非批处理快路径

function unbatchedExecute(_extra: UnbatchedExecutionExtra, list: any[]) { return list?.[0]; }

非批处理(unbatched)模式下直接取list[0];若列表为空或为null?.保证结果是undefined而不是抛错。

3.2 批处理最优路径

function optimalExecute<TData>({ indexMap, values: [values0], }: ExecutionDetails<[ReadonlyArray<TData>]>): GrafastResultsList<TData> { return indexMap((i) => values0.at(i)?.[0]); }

批处理场景下,Grafast 一次为多个“行”求值(例如 GraphQL 连接中的每个父节点各取一次首项)。optimalExecute借助indexMap对每个索引执行values0.at(i)?.[0],把“对数组取值并取首元素”合并为一次原子操作,避免逐项装箱与重复分发。

3.3 通用迭代器路径

isArray = false时,走类上的通用execute

execute({ indexMap, values: [values0] }): GrafastResultsList<TData> { return indexMap((i) => { const val = values0.at(i); if (val == null) return val; if (Array.isArray(val)) return val[0]; // Iterable? Return the first entry return (async () => { for await (const e of val) { return e; } return undefined; })(); }); }

注意该实现依然是双保险:即便传了false,如果实际值是数组仍走val[0];只有遇到真迭代器时才用for await...of拉取首项,迭代器为空时返回undefined。这也解释了文档为何要求迭代器场景显式传false——提前声明可让整个步骤保持同步安全,而迭代器必须异步消费,只能退化为异步结果。

四、计划优化:first(list([$a, $b]))直接化简为$a

FirstStep重写了optimize()(first.ts):

optimize() { const parent = this.getDep(0); // The first of a list plan is just the first dependency of the list plan. if (parent instanceof ListStep) { return parent.first(); } return this; }

当被取首项的父步骤本身就是ListStep(即“若干步骤的有序列表”步骤,见 grafast/grafast/src/steps/list.ts 的first()方法)时,first(list([$a, $b, ...]))会在优化阶段被直接替换为其第一个依赖$a,从而把“构造列表再取首项”这条链完全消除。

这一点在官方文档 step-classes.mdx 中被作为“计划化简(simplification)”的典型用例专门讲解:

Similarlyfirst(list([$a, $b]))can be simplified to just$a.

此外FirstStep还实现了[$$deepDepSkip]()(first.ts),告诉依赖分析器它的“深层依赖”就是那个列表步骤本身,配合allowMultipleOptimizations = true与恒等的deduplicate(peers)(first.ts),使得多个等价first调用可以在计划层安全合并、重复优化,而不会破坏执行顺序。

五、仓库内的真实落地用法

first不是孤立的教学示例,它在 dataplan-pg 与连接处理中被广泛使用:

  • PgSelectSingleStep(取单行):在 grafast/dataplan-pg/src/steps/pgSelect.ts 中:

    return new PgSelectSingleStep(this, first(this, true), options);

    显式传true,因为PgSelect的查询结果必然是数组,可安全启用数组优化。该用法同样出现在官方教程 step-library/dataplan-pg/pgSelect.md 与 step-library/dataplan-pg/pgSelect.md(const $firstUser = $users.row(first($users));)。

  • PgUnionAllSingleStep:在 grafast/dataplan-pg/src/steps/pgUnionAll.ts 中直接使用默认形态first(this)

  • 连接步骤的_items()路径:在 connection.ts 中,ConnectionStep需要从游标优化后的集合中取首元素时也会调用first($connection._items(), isArray),并依据场景动态决定是否走数组优化。

  • 导出位置first作为标准步骤从 grafast/grafast/src/index.ts 等处多次导出,配合FirstStep$$exportmoduleName: "grafast")可被graphile-export等工具序列化复用。

此外,standard-steps/list.md 也把.first()列为ListStep的内置方法之一,与独立的first步骤互为补充。

六、实践要点小结

  1. 默认用于数组:列表步骤产出的值是普通数组(或null/undefined)时,直接first($list),享受同步安全与values0.at(i)?.[0]快路径。
  2. 迭代器必须显式声明:传入(异步)迭代器时务必写first($iterable, false),否则会在运行时退化为异步路径,并失去isSyncAndSafe保证。
  3. 空列表语义:无论哪条路径,空数组/空迭代器都返回undefined,不会抛错,可安全用于可空字段。
  4. 交给优化器化简:不要手写first(list([...]))再担心开销——优化阶段会直接折叠为第一个依赖步骤。
  5. 结合row()使用:在 dataplan-pg 中$pgSelect.row(first($pgSelect))是“取查询结果首行”的标准姿势,参数true表明结果必为数组。

深入阅读建议:first.ts 完整实现、StepRepresentingList 类型定义、ListStep.first(),以及官方标准步骤目录 standard-steps。

【免费下载链接】crystal🔮 Graphile's Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询