core-js 中的 Await dictionary 提案:Promise.allKeyed 与 Promise.allSettledKeyed 完全指南
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
导读
Await dictionary 是 TC39 正在推进的 JavaScript 提案(proposal-await-dictionary),其核心目标是为开发者提供一种“按对象键名组织 Promise、统一等待结果”的并发原语:Promise.allKeyed与Promise.allSettledKeyed。本文以 docs/web/docs/features/proposals/await-dictionary.md 为骨架,结合 core-js 仓库中的模块源码与 QUnit 测试用例,系统讲解这两个方法的类型签名、语义细节、安装入口、使用示例与底层实现原理,帮助你直接在当前项目中使用这一提案能力。
提案背景:为什么需要 "Keyed" 并发方法
在现有标准 API 中,Promise.all接受一个可迭代对象(通常是数组),结果也以数组形式返回。当并发任务的来源是一组带名字的配置项、命名资源或字典结构时,开发者不得不在“数组 + 手动映射回键名”之间来回转换,既啰嗦又容易在索引上出错。
Await dictionary 提案的出发点是:允许你直接传入一个普通对象(字典),键名即结果键名。Promise.allKeyed在语义上相当于Object.fromEntries与Promise.all的组合,而Promise.allSettledKeyed则是“keyed 版”的Promise.allSettled,即使个别 Promise 失败,也会收集所有键的最终状态而不会整体拒绝。
从源码注释可以看到,core-js 正是依据该提案实现这两个方法的,见 esnext.promise.all-keyed.js 与 esnext.promise.all-settled-keyed.js 中的注释:// Promise.allKeyed method/// Promise.allSettledKeyed method,均标注提案地址https://github.com/tc39/proposal-await-dictionary。
模块与内置方法签名
原文档明确了该提案在 core-js 中由两个模块承载:
| 模块文件(相对仓库根目录) | 提供的方法 |
|---|---|
| esnext.promise.all-keyed.js | Promise.allKeyed |
| esnext.promise.all-settled-keyed.js | Promise.allSettledKeyed |
对应到原生内置对象的 TypeScript 签名如下(继承自原文档):
class Promise { allKeyed<T extends Record<string, unknown>>( obj: T ): Promise<{ [K in keyof T]: Awaited<T[K]> }>; allSettledKeyed<T extends Record<string, unknown>>( obj: T ): Promise<{ [K in keyof T]: PromiseSettledResult<Awaited<T[K]>> }>; }需要说明的语义要点:
allKeyed返回的 Promise 在所有键对应的值全部 resolve 后兑现;只要有一个值 reject,整个结果就会以该错误 reject(与Promise.all的“快速失败”行为一致)。allSettledKeyed永不因单个值 reject 而整体拒绝,而是为每个键生成{ status: 'fulfilled', value }或{ status: 'rejected', reason }形式的PromiseSettledResult(与Promise.allSettled行为一致)。- 两者都接受普通值(非 Promise 值会被直接当作已兑现处理),这一点在测试 esnext.promise.all-keyed.js 中有明确验证。
使用示例
以下是原文档给出的标准示例,直接展示了“字典输入、字典输出”的用法:
await Promise.allKeyed({ a: Promise.resolve(1), b: Promise.resolve(2), c: 3, }); // => { a: 1, b: 2, c: 3 } await Promise.allSettledKeyed({ a: Promise.resolve(1), b: Promise.reject(2), c: 3, }); // => { a: { status: "fulfilled", value: 1 }, b: { status: "rejected", reason: 2 }, c: { status: "fulfilled", value: 3 } }典型场景:带名字的并发请求
假设需要同时请求三个具名资源并保持结构对应:
const result = await Promise.allKeyed({ user: fetch('/api/user').then(r => r.json()), posts: fetch('/api/posts').then(r => r.json()), settings: fetch('/api/settings').then(r => r.json()), }); // result.user / result.posts / result.settings 一一对应,无需再手动 zip相比Promise.all返回数组后再按索引取值,allKeyed让每个异步结果天然拥有语义化键名,代码可读性与可维护性都更好。
安装与使用入口
原文档给出了三种入口方式(Entry points):
core-js/proposals/await-dictionary core-js(-pure)/actual|full/promise/all-keyed core-js(-pure)/actual|full/promise/all-settled-keyed它们的实际对应关系与使用姿势如下。
1. 提案聚合入口:一次引入两个方法
仓库中的 packages/core-js/proposals/await-dictionary.js 是一个聚合模块,内部仅做两件事:
require('../modules/esnext.promise.all-keyed'); require('../modules/esnext.promise.all-settled-keyed');因此你只需一次性引入即可同时获得两个方法:
import 'core-js/proposals/await-dictionary'; // 或 CommonJS require('core-js/proposals/await-dictionary'); await Promise.allKeyed({ a: Promise.resolve(1) }); await Promise.allSettledKeyed({ a: Promise.resolve(1) });2. 单一方法入口:按需加载
若只想引入其中一个方法,可以使用精确到方法的入口(以 actual/full 版本为例,文件位于 packages/core-js/actual/promise/all-keyed.js 与 packages/core-js/actual/promise/all-settled-keyed.js):
import 'core-js/actual/promise/all-keyed'; import 'core-js/actual/promise/all-settled-keyed'; // 或 require('core-js/actual/promise/all-keyed'); require('core-js/actual/promise/all-settled-keyed');full/与actual/的区别在于:actual入口只包含按提案最新状态可用的实现,full则包含 core-js 的全部特性集。core-js-pure版本(不污染全局原型)同样支持这两个入口,只是引入前缀为core-js-pure/。关于入口点的完整说明,可参考 docs/web/docs/usage.md 中 “Entry points” 一节的约定。
// core-js-pure 版本示例 const { allKeyed } = require('core-js-pure/actual/promise/all-keyed');源码级实现原理
allKeyed 的核心流程
esnext.promise.all-keyed.js 的实现要点如下:
- 通过
newPromiseCapabilityModule.f(C)创建 Promise 能力(capability),this指向调用时的构造函数,保证子类化语义。 - 通过
Reflect.ownKeys拿到输入对象的全部自有键(含 Symbol 键),再用getOwnPropertyDescriptor过滤出可枚举(enumerable)自有属性——这一点与Object.keys的语义保持一致。 - 对每个可枚举键调用
C.resolve(promises[key])统一包装为 Promise(普通值也会被 resolve),随后.then注册兑现回调。 - 使用
remaining计数器跟踪未完成数量,全部完成后用create(null)构造一个null 原型的结果对象,并通过createProperty按原键名回填结果,最终resolve(res)。 - 任意一个值 reject 时直接调用
reject,整体失败。
allSettledKeyed 的差异
esnext.promise.all-settled-keyed.js 与allKeyed结构几乎一致,唯一区别在于第 33-45 行的createElementResolver辅助函数:它对每个值同时注册fulfilled与rejected两条回调路径,无论结果如何都会把{ status: 'fulfilled', value }或{ status: 'rejected', reason }写入对应位置,因此整体永远不会因单个值的失败而拒绝。
关键行为总结(均有测试佐证)
仓库中的单元测试(tests/unit-global/esnext.promise.all-keyed.js 与 tests/unit-global/esnext.promise.all-settled-keyed.js)对以下行为做了完整覆盖:
| 行为 | 说明 | 测试位置 |
|---|---|---|
| 支持原始值 | 非 Promise 值直接按已兑现处理 | 两个测试文件的 "resolved with primitives" 用例 |
| 空对象 | 传入{}立即兑现为空对象 | "resolved with empty object" 用例 |
| 忽略不可枚举与原型属性 | 只处理输入对象自身的可枚举键,原型链上的键被忽略 | "resolved with hidden attributes" 用例 |
| Symbol 键支持 | 结果对象保留 Symbol 键 | "symbol keys" 用例 |
| 结果顺序稳定 | 即使 Promise 以不同顺序完成,结果键顺序仍与输入一致(依赖索引槽位机制) | "keys order" 用例 |
| 结果对象 null 原型 | Object.getPrototypeOf(result) === null | "result object has null prototype" 用例 |
| 子类化支持 | allKeyed.call(SubPromise, ...)返回SubPromise实例 | "subclassing" 用例 |
| 非法输入拒绝 | 传入字符串等非对象会以TypeError拒绝 | "rejected on incorrect input" 用例 |
以“隐藏属性”用例为例,输入对象Object.create({ proto: Promise.resolve('hidden') })且含一个enumerable: false的invisible键时,结果只包含{ visible: 42 },证明实现严格遵循“仅可枚举自有键”的语义。
延迟兑现下的键序保持
由于allKeyed在遍历时用keys[index] = key和values[index] = undefined预先占位,再在回调中按index回填,因此即便较慢的 Promise 最后完成,结果对象的键顺序依然等于输入对象的键顺序。测试 "keys order" 专门构造了b延迟 10ms 兑现的场景来验证这一点。
与 Promise.all / allSettled 的对比
| 特性 | Promise.all | Promise.allKeyed | Promise.allSettled | Promise.allSettledKeyed |
|---|---|---|---|---|
| 输入形态 | 可迭代对象(数组等) | 对象(字典) | 可迭代对象 | 对象(字典) |
| 输出形态 | 数组 | 按原键名组织的对象 | PromiseSettledResult[]数组 | 按原键名组织的PromiseSettledResult对象 |
| 快速失败 | 是 | 是 | 否(汇总所有状态) | 否(汇总所有状态) |
| 键名语义 | 无(索引定位) | 有(语义化定位) | 无 | 有 |
如果你的并发任务天然以“命名资源”形式存在(配置加载、具名请求、分组校验等),keyed 系列方法能显著减少“数组 ↔ 字典”之间的人工映射代码。
注意事项与版本现状
- 该能力目前属于esnext(提案)阶段特性,对应模块文件均以
esnext.前缀命名,尚未进入标准库;依赖原生Promise.allKeyed的代码在绝大多数运行时中不可用,必须通过 core-js 或 core-js-pure 引入。 - 引入后是向
Promise构造函数添加静态方法,位于 packages/core-js/modules/esnext.promise.all-keyed.js 的$({ target: 'Promise', stat: true }, ...)中,stat: true表示挂载为静态方法而非原型方法。 - 在
core-js-pure(不污染全局)场景下,方法通过 packages/core-js/actual/promise/all-keyed.js 中的包装函数导出:调用时若this可调用则作为构造上下文传入,否则回退到原生Promise。 - 由于提案仍在演进,具体语义(如是否处理 Symbol 键、结果对象原型等)以当前 core-js 版本实现与 TC39 提案最新文本为准。
小结
Await dictionary 提案为 JavaScript 并发编程补上了一块实用拼图:Promise.allKeyed让你用字典组织并发任务并保持键名语义,Promise.allSettledKeyed则提供“全量汇总、永不整体失败”的 keyed 版 allSettled。通过 packages/core-js/proposals/await-dictionary.js 或两个独立入口即可在当前项目中启用,其内部实现与测试用例(esnext.promise.all-keyed.js、esnext.promise.all-settled-keyed.js)为理解语义边界(可枚举自有键、Symbol 键、null 原型、键序稳定、子类化)提供了完整的可验证依据,值得在涉及命名并发任务的场景中优先采用。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考