core-js 中的 AsyncIterator helpers 提案:异步迭代器工具方法全解析
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
导读
本文聚焦 core-js 对 TC39 AsyncIterator helpers 提案(异步迭代器工具方法)的完整实现,涵盖其提供的AsyncIterator全局构造器、from/drop/map/filter/reduce/toArray等 13 个工具模块、全部入口点的引入方式,以及core-js/configurator中USE_FUNCTION_CONSTRUCTOR与AsyncIteratorPrototype两个关键配置的深层原理。阅读完本文,你将掌握在任意支持 ES2018+ 的环境中以标准方式组合异步迭代器变换管道(chain),并能在core-js-pure与 CSP 受限环境等边界条件下正确选用配置。
该特性当前属于早期阶段提案(early-stage proposal),core-js 将其置于
esnext.*命名空间下,仅通过proposals/与full/等入口暴露,不属于stable与actual的稳定面。
提案背景:为什么需要 AsyncIterator helpers
同步侧已经通过 Iterator helpers 提案 为普通迭代器提供了map、filter、take等"懒求值"工具方法;异步侧的 AsyncIterator helpers 是它的自然延伸,目标是让AsyncIterator(异步迭代器)也能享受同样的组合式编程体验:不提前物化数据、按需拉取(pull-based)、可提前终止(early exit)。
core-js 将整个提案拆成了 14 个独立模块(1 个构造器 + 13 个工具方法),分布在 packages/core-js/modules 下:
| 模块文件 | 作用 |
|---|---|
| esnext.async-iterator.constructor.js | 定义全局AsyncIterator抽象构造器 |
| esnext.async-iterator.from.js | 静态方法AsyncIterator.from(iterable) |
| esnext.async-iterator.drop.js | drop(limit)跳过前 N 个元素 |
| esnext.async-iterator.every.js | every(callbackfn)全部满足 |
| esnext.async-iterator.filter.js | filter(callbackfn)过滤 |
| esnext.async-iterator.find.js | find(callbackfn)查找首个匹配 |
| esnext.async-iterator.flat-map.js | flatMap(callbackfn)扁平化映射 |
| esnext.async-iterator.for-each.js | forEach(callbackfn)遍历消费 |
| esnext.async-iterator.map.js | map(callbackfn)映射 |
| esnext.async-iterator.reduce.js | reduce(callbackfn, initialValue)归约 |
| esnext.async-iterator.some.js | some(callbackfn)任一满足 |
| esnext.async-iterator.take.js | take(limit)取前 N 个 |
| esnext.async-iterator.to-array.js | toArray()物化为数组 |
| esnext.iterator.to-async.js | 同步迭代器的toAsync()适配 |
入口聚合文件 packages/core-js/proposals/async-iterator-helpers.js 依次require上述全部模块,一次性注册整个提案。
内置 API 签名:一份完整的 TypeScript 视图
关联文档给出了提案的完整 TS 签名,这是理解整个特性面的最佳起点:
class Iterator { toAsync(): AsyncIterator<any>; } class AsyncIterator { static from(iterable: AsyncIterable<any> | Iterable<any> | AsyncIterator<any>): AsyncIterator<any>; drop(limit: uint): AsyncIterator<any>; every(async callbackfn: (value: any, counter: uint) => boolean): Promise<boolean>; filter(async callbackfn: (value: any, counter: uint) => boolean): AsyncIterator<any>; find(async callbackfn: (value: any, counter: uint) => boolean)): Promise<any>; flatMap(async callbackfn: (value: any, counter: uint) => AsyncIterable<any> | Iterable<any> | AsyncIterator<any>): AsyncIterator<any>; forEach(async callbackfn: (value: any, counter: uint) => void): Promise<void>; map(async callbackfn: (value: any, counter: uint) => any): AsyncIterator<any>; reduce(async callbackfn: (memo: any, value: any, counter: uint) => any, initialValue: any): Promise<any>; some(async callbackfn: (value: any, counter: uint) => boolean): Promise<boolean>; take(limit: uint): AsyncIterator<any>; toArray(): Promise<Array>; @@toStringTag: 'AsyncIterator' }解读其中的关键设计:
- 回调都是异步的:
callbackfn可以是返回 Promise 的异步函数,也可以返回普通值,core-js 内部统一通过Promise.resolve收编(见下文实现剖析); - 所有终止类方法返回
Promise:every/find/forEach/reduce/some/toArray返回Promise<T>,只有流式变换方法(drop/filter/flatMap/map/take)返回新的AsyncIterator,可以继续链式调用; @@toStringTag为'AsyncIterator':由 esnext.async-iterator.constructor.js 中的createNonEnumerableProperty在%AsyncIteratorPrototype%上以不可枚举方式定义,保证Object.prototype.toString行为符合规范且不污染枚举。
AsyncIterator构造器本身是抽象类
从 esnext.async-iterator.constructor.js 可以看到,直接调用new AsyncIterator()会抛出TypeError: Abstract class AsyncIterator not directly constructable:
var AsyncIteratorConstructor = function AsyncIterator() { anInstance(this, AsyncIteratorPrototype); if (getPrototypeOf(this) === AsyncIteratorPrototype) throw new $TypeError('Abstract class AsyncIterator not directly constructable'); };也就是说,AsyncIterator只作为所有异步迭代器的公共原型基座,实例必须来自异步生成器、AsyncIterator.from、toAsync()或原生异步可迭代对象。
入口点:按需引入的四种粒度
关联文档列出的入口点覆盖了proposals、actual、full三个命名空间(stable与es不含早期提案),对应 docs/web/docs/usage.md 中描述的"按需 polyfill"约定:
core-js/proposals/async-iterator-helpers core-js(-pure)/actual|full/async-iterator core-js(-pure)/actual|full/async-iterator/drop core-js(-pure)/actual|full/async-iterator/every core-js(-pure)/actual|full/async-iterator/filter core-js(-pure)/actual|full/async-iterator/find core-js(-pure)/actual|full/async-iterator/flat-map core-js(-pure)/actual|full/async-iterator/for-each core-js(-pure)/actual|full/async-iterator/from core-js(-pure)/actual|full/async-iterator/map core-js(-pure)/actual|full/async-iterator/reduce core-js(-pure)/actual|full/async-iterator/some core-js(-pure)/actual|full/async-iterator/take core-js(-pure)/actual|full/async-iterator/to-array core-js(-pure)/actual|full/iterator/to-async使用建议:
- 一次性引入整套提案:
import 'core-js/proposals/async-iterator-helpers'(或在core-js-builder自定义构建中选择该 feature); - 按方法精确引入:如
import 'core-js/actual/async-iterator/map',只注入用到的模块,适合对包体积敏感的项目; actual与full的取舍:actual包含 stage 3 及已落地的提案特性,full则额外包含 stage 2 等更早期提案。按 docs/web/docs/usage.md 的官方建议,生产环境优先使用/actual/;由于 AsyncIterator helpers 长期处于 stage 2.7,实践中通常仍通过/full/或proposals/引入;core-js-pure变体:将core-js替换为core-js-pure即可在不污染全局的前提下使用,适合库作者。
实操示例:异步变换管道
关联文档给出了两个经典示例,完整还原如下(注意toArray()的await):
await AsyncIterator.from([1, 2, 3, 4, 5, 6, 7]) .drop(1) .take(5) .filter(it => it % 2) .map(it => it ** 2) .toArray(); // => [9, 25]执行过程推演:from包装数组迭代器 →drop(1)跳过1→take(5)限流为[2,3,4,5,6]→filter保留奇数[3,5]→map平方得[9,25]→toArray物化。整个链是惰性的:只有调用toArray()才开始真正从上游拉取数据。
await [1, 2, 3].values().toAsync().map(async it => it ** 2).toArray(); // => [1, 4, 9]第二个示例展示了Iterator.prototype.toAsync():将同步迭代器[].values()转换为异步迭代器,再map一个返回 Promise 的异步回调。注意这里也可以直接写await it ** 2,因为map的回调返回值无论是否为 Promise 都会被正确 await。
异步回调的真实行为
下面验证"回调可以返回 Promise"这一核心设计。先看 packages/core-js/internals/async-iterator-map.js 的实现思路:它通过Promise.resolve(call(callback, state.iterator, value, counter++))把回调结果统一收编为 Promise,再传入next生成器的yield表达式——异步函数yield await promise的行为恰好实现了"等待回调完成后再取下一个值"。这正是天然支持异步回调的原因:
const result = await AsyncIterator .from([1, 2, 3]) .map(async value => { await sleep(10); // 模拟异步耗时操作 return value * 10; }) .toArray(); console.log(result); // => [10, 20, 30]从源码看实现原理:代理与内部状态
core-js 的这一整套实现并非简单地在%AsyncIteratorPrototype%上挂方法,而是构建了两层精巧的基础设施,集中在 packages/core-js/internals/async-iterator-create-proxy.js:
内部状态(Internal State):每个工具方法产生的包装异步迭代器都携带
state,包含iterator(上游迭代器)、next(上游 next 方法)、nextHandler(本方法特有逻辑)、counter(元素下标,从 0 计数)以及done标志。InternalStateModule以非枚举方式把状态挂在实例上,避免属性名冲突。两类原型:
WrapForValidAsyncIteratorPrototype:由AsyncIterator.from/toAsync()产生的"纯包装器"(如 packages/core-js/internals/async-iterator-wrap.js 所示,next直接转发this.next);AsyncIteratorHelperPrototype:由drop/map等方法产生的"助手迭代器",其@@toStringTag被定义为'Async Iterator Helper'。
提前终止(early exit):
return()方法实现了完整的资源回收——先尝试关闭内部迭代器(flatMap场景下的inner),再关闭外部迭代器,并正确处理上游return方法缺失、Promise 拒绝等情况(见 async-iterator-create-proxy.js)。这保证了for await...of中途break或reduce提前完成时不会泄漏迭代器资源。
drop的惰性跳过实现
esnext.async-iterator.drop.js 展示了一个典型实现:drop并不立刻消费上游,而是在nextHandler中维护state.remaining计数器,每次被拉取时才跳过,直到跳满limit才返回第一个真实值:
if (anObject(step).done) { state.done = true; resolve(createIterResultObject(undefined, true)); } else if (state.remaining) { state.remaining--; loop(); } else resolve(createIterResultObject(step.value, false));limit参数经过toPositiveInteger(notANaN(+limit))规范化:NaN抛错、负值按 0 处理、小数向下取整(来自to-positive-integer内部模块)。
from的多态适配
esnext.async-iterator.from.js 接受AsyncIterable | Iterable | AsyncIterator | string四种输入,其底层是 packages/core-js/internals/get-async-iterator-flattenable.js:
- 优先读取
obj[Symbol.asyncIterator];若不存在,回退到同步的Symbol.iterator,并通过 packages/core-js/internals/async-from-sync-iterator.js 将同步迭代器适配为异步迭代器(next()返回 Promise,且 Promise 被拒绝时主动对同步迭代器调用throw关闭); - 字符串会被
toObject包装后走同样的流程,因此AsyncIterator.from('abc')也是合法的; - 若输入本身已经继承
%AsyncIteratorPrototype%,from直接原样返回,不做多余包装。
reduce的健壮性设计
esnext.async-iterator.reduce.js 是最复杂的一个终止类方法,值得关注的点:
- 缺省
initialValue时以首个元素作为累加器;对空迭代器且无初始值的情况,明确拒绝(TypeError: Reduce of empty iterator with no initial value),与数组reduce语义一致; - 每一步的
reducer结果如果是对象(可能为 thenable),会Promise.resolve(result).then(handler, ...)异步接续,否则同步进入下一轮; - 全程通过
closeAsyncIteration在出错时关闭上游迭代器,避免悬挂。
原型获取的兼容性方案:Caveats 深挖
关联文档的 Caveats 部分是全文最有价值的技术细节,它揭示了 core-js 在老浏览器上获取真实%AsyncIteratorPrototype%的艰难取舍。
问题根源
- 在
core-js-pure(IS_PURE)下,为避免原型污染,新方法不会添加到真实%AsyncIteratorPrototype%上,而只存在于包装器。因此不要写[].values().toAsync().map(fn),而应使用AsyncIterator.from([]).map(fn)——后者返回的就是 core-js 自己的包装器,方法都在上面。 - 在老浏览器中,core-js 只有在能访问异步生成器语法时才拿得到真实原型。源码 packages/core-js/internals/async-iterator-prototype.js 给出了完整的分支逻辑:
if (PassedAsyncIteratorPrototype) { // 1. configurator 传入的原型 AsyncIteratorPrototype = PassedAsyncIteratorPrototype; } else if (isCallable(AsyncIterator)) { // 2. 环境原生存在 AsyncIterator AsyncIteratorPrototype = AsyncIterator.prototype; } else if (shared[USE_FUNCTION_CONSTRUCTOR] || globalThis[USE_FUNCTION_CONSTRUCTOR]) { try { // 3. Function 构造器动态生成 async generator prototype = getPrototypeOf(getPrototypeOf(getPrototypeOf(Function('return async function*(){}()')()))); if (getPrototypeOf(prototype) === Object.prototype) AsyncIteratorPrototype = prototype; } catch (error) { /* empty */ } } if (!AsyncIteratorPrototype) AsyncIteratorPrototype = {}; else if (IS_PURE) AsyncIteratorPrototype = create(AsyncIteratorPrototype);分支 3 是兼容老浏览器的关键:用Function构造器在运行时动态编译async function*(){}(),通过三次getPrototypeOf拿到%AsyncIteratorPrototype%(async generator 实例 → generator 原型 → 再上一层才是 AsyncIterator 原型)。代价是Function构造器在 CSP(内容安全策略)环境下会被拦截,导致整个降级路径失效,最终退回{}(功能退化为仅存在于包装器上)。
方案一:开启USE_FUNCTION_CONSTRUCTOR
关联文档给出的第一种解法是显式开启开关(注意文档原文将选项写在configurator调用中,且USE_FUNCTION_CONSTRUCTOR的取值会被configurator按布尔语义处理):
const configurator = require('core-js/configurator'); configurator({ USE_FUNCTION_CONSTRUCTOR: true }); require('core-js/actual/async-iterator'); (async function * () { /* empty */ })() instanceof AsyncIterator; // => true此时async-iterator-prototype.js会走分支 3,尝试用Function构造器抓取真实原型;instanceof AsyncIterator成立说明原生 async generator 实例成功接入了 core-js 扩展的%AsyncIteratorPrototype%。适用场景:不启用 CSP 或 CSP 允许unsafe-eval的环境。
方案二:直接注入原型对象
作为替代,关联文档演示了绕过Function的纯运行时方案——把自己从 async generator 上现取的原型交给configurator:
const configurator = require('core-js/configurator'); const { getPrototypeOf } = Object; configurator({ AsyncIteratorPrototype: getPrototypeOf(getPrototypeOf(getPrototypeOf(async function * () { /* empty */ }()))) }); require('core-js/actual/async-iterator'); (async function * () { /* empty */ }()) instanceof AsyncIterator; // => true这一方案在async-iterator-prototype.js中命中分支 1(PassedAsyncIteratorPrototype),完全避免Function构造器,因此不受 CSP 限制,但要求运行环境本身已经支持 async generator 语法。两种方案的效果等价:让真实的%AsyncIteratorPrototype%获得 core-js 注入的 helpers,使任何原生 async generator 产物都能直接使用map/filter等方法。
configurator内部如何接收这些选项
从 packages/core-js/configurator.js 可以看到,configurator除了处理useNative/usePolyfill/useFeatureDetection三个激进等级选项外,专门为这两个键做了透传:
if (hasOwn(options, USE_FUNCTION_CONSTRUCTOR)) { shared[USE_FUNCTION_CONSTRUCTOR] = !!options[USE_FUNCTION_CONSTRUCTOR]; } if (hasOwn(options, ASYNC_ITERATOR_PROTOTYPE)) { shared[ASYNC_ITERATOR_PROTOTYPE] = options[ASYNC_ITERATOR_PROTOTYPE]; }两者都写入shared-store(core-js 内部共享的单例存储),随后被async-iterator-prototype.js读取。需要注意:
configurator必须在任何 core-js 模块加载之前调用(源码注释与 docs/web/docs/usage.md 均强调加载顺序);USE_FUNCTION_CONSTRUCTOR也可通过全局globalThis.USE_FUNCTION_CONSTRUCTOR直接设置(见 async-iterator-prototype.js);- 传入的
AsyncIteratorPrototype对象必须形如真实的%AsyncIteratorPrototype%,否则instanceof判断可能不成立。
测试覆盖:如何在仓库中验证
tests/unit-global目录下提供了与提案一一对应的单元测试文件(同样适用于tests/unit-pure的 pure 版本),可作为行为规范的活文档:
- tests/unit-global/esnext.async-iterator.constructor.js
- tests/unit-global/esnext.async-iterator.drop.js
- tests/unit-global/esnext.async-iterator.every.js
- tests/unit-global/esnext.async-iterator.filter.js
- tests/unit-global/esnext.async-iterator.find.js
- tests/unit-global/esnext.async-iterator.flat-map.js
- tests/unit-global/esnext.async-iterator.for-each.js
- tests/unit-global/esnext.async-iterator.from.js
- tests/unit-global/esnext.async-iterator.map.js
- tests/unit-global/esnext.async-iterator.reduce.js
- tests/unit-global/esnext.async-iterator.some.js
- tests/unit-global/esnext.async-iterator.take.js
- tests/unit-global/esnext.async-iterator.to-array.js
- tests/unit-global/esnext.iterator.to-async.js
在仓库根目录执行测试(如npm run test-unit-global或按项目 CONTRIBUTING.md 的指引)即可验证这些行为,尤其是回调返回 Promise、提前终止、@@toStringTag与原型接入等边界情况。
实战选型建议
综合以上分析,在真实项目中引入 AsyncIterator helpers 时可以参考以下决策:
| 场景 | 推荐入口 / 配置 |
|---|---|
| 现代浏览器 + Node.js,希望全局可用 | import 'core-js/actual/async-iterator'(或full/以覆盖更早期提案) |
| 按需精确引入单个方法 | import 'core-js/actual/async-iterator/map' |
| 库作者,避免全局污染 | core-js-pure变体,并用AsyncIterator.from(...)链式调用 |
老浏览器 + 允许Function构造器 | 先configurator({ USE_FUNCTION_CONSTRUCTOR: true })再加载模块 |
| CSP 严格限制、但支持 async generator | 先configurator({ AsyncIteratorPrototype: <真实原型> })再加载模块 |
环境既不支持 async generator 也没有AsyncIterator | 保持默认(退回包装器实现),放弃真实原型接入 |
需要再次强调的是:该提案目前仍是早期阶段提案,API 细节可能随规范演进而变化;core-js 通过esnext.*命名空间与proposals/入口将其与稳定特性隔离,升级 core-js 小版本时应关注 CHANGELOG.md 中关于该提案的改动记录。
【免费下载链接】core-jsStandard Library项目地址: https://gitcode.com/GitHub_Trending/co/core-js
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考