JAVASCRIPT与TYPESCRIPT · KNOWLEDGE CHAPTER
K09

异步控制与迭代

异步代码至少有四个独立问题:工作何时启动、结果何时被消费、失败如何传播、资源怎样停止。Promise 描述结果依赖,不是线程或取消器;迭代协议描述推进节奏,模块描述初始化依赖,宿主事件循环决定回调何时获得执行机会。把这些边界分开,才能设计正确的并发与清理。

JavaScript与TypeScript2 道章节练习8 道主练关联

核心模型

异步代码至少有四个独立问题:工作何时启动、结果何时被消费、失败如何传播、资源怎样停止。Promise 描述结果依赖,不是线程或取消器;迭代协议描述推进节奏,模块描述初始化依赖,宿主事件循环决定回调何时获得执行机会。把这些边界分开,才能设计正确的并发与清理。

原理与机制

结果组合必须与成功条件一致

then 返回新 Promise,回调的返回、抛错或返回的 Promise 决定后续状态,因此必须 return/await 需要跟踪的工作。all 按输入顺序交付结果,首个拒绝便拒绝但不回滚或取消其他任务;allSettled 收齐每项,race 取首个落定,any 取首个 fulfilled,全部拒绝时给 AggregateError。Promise.finally 的普通返回不替换原结果,但抛错或返回拒绝会覆盖,异步清理也会延迟后续;语法 try/finally 内 return 却能覆盖原 return 或 throw。主失败和清理失败应按契约分别保留。

启动控制与取消都在组合器之外

forEach 丢弃回调返回值,自身为 undefined,所以 await forEach(async...) 不等待保存完成。串行用 for...of 内 await,少量独立任务用 all(map(...));数量大时保存未启动工厂,用池限制 active,已 map(fetch) 后再分批 await 为时已晚。镜像竞速应在每个候选内部完成 HTTP 状态、解析、业务验证后才 fulfilled,再用 any。获胜数据完整验证后可取消其他请求;Promise.race 超时只结束等待,底层仍需 AbortSignal。写操作不能因超时便假定没成功,重试要考虑幂等与状态查询。

迭代把生产节奏暴露给消费者

可迭代对象的 Symbol.iterator 返回提供 next 的迭代器,生成器把暂停点与局部状态封装起来;for...of 消费值,for...in 枚举字符串属性。异步 next 返回 Promise,for await...of 等待当前结果及循环体后再推进,只有生产者也按需发请求且缓冲有界时才形成实际背压。消费者 break 会尝试 return,生成器 finally 可释放资源;生产者 next 自己抛错仍需自行兜底。return 不会打断正在 await 的挂起 fetch,必须让底层取消使等待落定。生成器也不会自动支持回看或重放。

模块共享绑定,也共享初始化依赖

ESM import 连接导出绑定,导出方更新后可被后续读取;导入方不能重新赋值该绑定,但可变对象内部仍可能被修改。循环依赖先建立绑定再求值,不一定失败,初始化时读取另一模块 TDZ 才是典型风险。顶层 await 让依赖模块等待,不冻结整个线程,却可引入复杂等待环。初始化最好由清楚入口管理;并发 init 需缓存进行中 Promise,而非只缓存结果,失败是否清缓存重试、不同配置怎样分键、单个等待者取消是否影响共享任务,都要明确。

调度顺序必须带宿主和执行上下文

同步 JavaScript 不被普通异步回调中途打断。Node 没有浏览器页面渲染机会,poll 处理 I/O、check 执行 setImmediate,timer 达阈值也不代表准点。libuv 1.45/Node 20 起每轮 timers 在 poll 后处理,首次进入循环前仍保留一次处理。常见 CJS 顶层 nextTick 先于 Promise/queueMicrotask;ESM 顶层已在异步求值中,微任务可先执行。顶层 timeout(0) 与 immediate 不承诺顺序,同一 I/O 回调中新建时 immediate 在前。递归微任务或 nextTick 可饿死 I/O,async 不把 CPU 工作变成并行。

最小示例

javascript
javascript
// 保存为 demo.mjs,演示组合结果,不限制并发。
const starts = [];
const jobs = [
  () => { starts.push('A'); return Promise.resolve('ok'); },
  () => { starts.push('B'); return Promise.reject(new Error('bad')); }
];
const results = await Promise.allSettled(jobs.map(start => start()));
console.log(starts.join(','), results.map(r => r.status).join(','));
console.log(await Promise.resolve(1).finally(() => 2));
try { await Promise.resolve(1).finally(() => { throw new Error('cleanup'); }); }
catch (e) { console.log(e.message); }

预期输出 A,B fulfilled,rejected,然后是 1 和 cleanup。任务在调用工厂时启动,allSettled 仅收集结果;普通 finally 返回保持 1,抛错则改变链结果。例子不访问网络,不代表已验证取消、限流或 Node 各阶段顺序;这些需要分别构造有可观测 active 和清理日志的测试。

环境:现代 Node.js;保存为 .mjs 以允许顶层 await。

核验:未执行教学示例;说明中的结果为机制推演,不是本轮测试通过记录。

边界与取舍

边界

  • all 不控制并发也不提供事务;any 的 fulfilled 不等于 HTTP 或业务成功。
  • 超时与取消不同;异步生成器 return 不能强行中断任意 await。
  • 不能把 CJS 顶层顺序推广到 ESM 或任意微任务内部,更不能把浏览器简化为 Node 阶段图。

取舍

  • 并行缩短独立任务总等待,却增加负载与部分成功状态;串行更易控制但可能浪费可用并发。
  • 惰性迭代降低预先请求与内存,回看、预取和共享初始化则需要额外缓存及取消所有权设计。

口述示范

我先写清启动、成功、失败和取消契约,再选 all、allSettled、any 或串行。限制并发必须控制任务工厂,取消必须进入底层协议;迭代和模块再分别管理清理与初始化,调度输出则明确 Node 版本、CJS/ESM 和创建回调的位置。

章节练习

练习 1

假设 TranslateFlow 从两个镜像读取配置,同时批量保存条目,分别怎样设计成功与重试?

展开检查点
  • 镜像每条链内部完整校验再 any,获胜后取消其余幂等读取。
  • 保存按任务工厂限流;稳定条目 id 与幂等键管理部分成功,收尾错误不覆盖根因。
练习 2

逐页导出可取消,模块初始化可共享,怎样避免挂起和重复请求?

展开检查点
  • 异步迭代按需请求并有界缓冲,底层 signal 配合 finally;不依赖 return 打断 fetch。
  • 按配置缓存进行中 init Promise,明确失败重试和取消所有权;调度实验区分 CJS、ESM、I/O 上下文。

参考来源与核验边界

核验日期 2026-10-04。Node 在线 API 页面可能随 Current 版本更新,排序结论限正文指定上下文。原题 X02 已记录 Node v24.19.0 的受控测试,本章不把历史测试冒充当前新增示例执行。 本章新增示例未在本轮执行,不新增测试通过声明。

内容版本:2026-10-04.2 · 题目和来源 ID 保持原样,本站不将面经标签解释为企业官方出题或高频保证。