编码、架构与项目 · KNOWLEDGE CHAPTER
K27

异步控制与事件

异步封装本质是小型状态机:决定工作何时启动、谁拥有结果、哪些调用共享工作、何时释放资源。Promise 只组合完成结果,取消信号只提出停止请求;启动限额与界面提交资格都要另建规则。

编码、架构与项目2 道章节练习8 道主练关联

核心模型

异步封装本质是小型状态机:决定工作何时启动、谁拥有结果、哪些调用共享工作、何时释放资源。Promise 只组合完成结果,取消信号只提出停止请求;启动限额与界面提交资格都要另建规则。

原理与机制

先写时间契约,再写计时器

尾沿防抖只在安静满 wait 后执行最后一次,节流则在持续调用中控制频率;首沿加尾沿需要说明单次调用是否重复、冷却从何时开始。maxWait 的最大期限不能随着每次输入无限后移。普通包装函数保留动态 this,回调使用最后参数,cancel 清理计时器及待处理状态。为防重入,执行用户回调前先登记本轮状态、清掉已取出的 pending,否则回调重新调用包装器时,外层可能覆盖下一轮工作。

启动控制与完成组合是两件事

并发池接收任务工厂,worker 在 await 前领取唯一索引,任务结束才补位,结果写回原索引以保留输入顺序。同步 throw 与异步 reject 都须捕获。Promise.all 则遍历输入、逐项安装处理器,以剩余计数和迭代结束哨兵处理空输入与乱序完成;它不限制已经启动的请求。有限迭代器中途抛错时,已产出的拒绝 Promise 仍要被接管,不能先整体展开后才注册。race 超时也不会停掉输家,补位可能使真实活动数超过 limit。

重试要绑定逻辑操作

有限重试先分类错误:参数、鉴权和业务拒绝通常不重试,暂时网络失败才考虑退避、抖动与总体截止时间,限流还应尊重服务端提示。可写操作必须幂等或使用稳定幂等键,重试沿用同一操作身份,改变载荷不能偷偷复用旧键。AbortSignal 要传给任务和退避等待,取消等待需清计时器与监听器;底层不合作时只能阻止晚到结果提交,不能宣称计算已停止。

共享执行和最新提交各有所有权

single-flight 的 Map 只保存进行中的 Promise,同 key 消费者共享它,成功失败都删除,并按 Promise 身份清理,防止旧任务删掉后来者。失败永不清除会让后续请求永久失败;一个消费者退出也不应直接取消所有消费者。latest-only 则给每轮 generation,新任务先取得身份再取消旧任务,并在启动及所有成功、失败、finally 回写前核对身份。A→B→A 三次文本相同不代表同一意图,序号防住不响应 abort 的旧转换。

事件派发也是可重入调度

Emitter 要声明派发时增删订阅的语义。若按快照执行,本轮新增等下一轮,本轮已入快照的监听器即使被删除仍执行;这不是原生 DOM 模型的复刻。once 在调用用户回调前设 fired 并退订,才能挡住嵌套 emit 与外层旧快照的再次调用。同步异常是否终止本轮、异步拒绝是否聚合也必须明确。组件和任务作用域负责退订、取消与释放流锁,避免事件总线成为无人负责的长期引用。

最小示例

javascript
javascript
function latestOnly(load, commit) {
  let generation = 0, controller;
  const run = async input => {
    const id = ++generation;
    const old = controller;
    const current = controller = new AbortController();
    old?.abort();
    if (id !== generation || current.signal.aborted) return;
    try {
      const value = await load(input, current.signal);
      if (id === generation && !current.signal.aborted)
        commit(value);
    } catch (error) {
      if (id === generation && !current.signal.aborted)
        throw error;
    }
  };
  run.cancel = () => { generation++; controller?.abort(); };
  return run;
}

让 A 慢、B 快,即使 A 忽略 signal,只有 B 可提交。再令取消 A 的监听器同步启动 C,可观察 B 在身份复查处退出,C 保持最新;这解释了“先登记新控制器,再取消旧任务”的必要性。commit 约定为短同步操作,调用方负责捕获当前错误。

核验:Node v24.19.0:A慢B快只提交B、取消监听器同步重入时只启动A/C断言通过;不代表网络取消或框架集成已测。

边界与取舍

边界

  • 防抖减少启动频率,不能防旧请求覆盖;all 拒绝、race 超时都不会自动取消底层任务。
  • 共享 Promise 不是结果缓存;缓存键若缺租户和权限,去重也会造成数据混用。
  • 包装器空间可为 O(1),但不合作的旧任务仍会耗资源,不能把不回写等同停止。

取舍

  • 并发越高可能缩短总时间,也可能放大连接、内存和限流压力;以真实 active 工作量选择上限。
  • 快照派发语义稳定且易解释,但额外占 O(监听器数) 空间;可变遍历省拷贝却更难推理。

口述示范

我先定义启动、共享、取消与提交四类契约,再画重入和乱序时间线。实现时用身份、不变量和资源清理保证状态正确,最后分别测真实并发数、结果顺序和过期回写。

章节练习

练习 1

并发池在 race 超时后马上补位,为什么 active 可能超过3?

展开检查点
  • 超时包装结束不代表底层结束。
  • 约定取消确认、继续占位或隔离资源,测试真实 active 上限。
练习 2

假设 TranslateFlow 切换 Provider 后旧分块追加到新文档,如何设计归属和清理?

展开检查点
  • 每轮记录请求ID、Provider、语言和文档ID,所有回写先核对。
  • 组件退出取消并退订;若使用 MV3 背景 worker,不将内存当持久任务状态。

参考来源与核验边界

2026-10-04 核验。示例不复刻 Promise 子类及全部规范细节;TranslateFlow 为假设演练,不是用户项目实现或结果。

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