JavaScript 生成器提前退出:for...of 的 break 怎样触发 finally 清理

10-01 4阅读

生成器适合逐条产生数据,但“调用方不再读取”和“生成器已经清理”并不是同一件事。读取第一条后就离开页面,如果调用方式只是不再执行 next,函数可能仍停在 yield 处。资源清理什么时候发生,需要看迭代是怎样结束的。

for...of 在 break 提前退出时,会按迭代关闭协议调用可用的 return 方法。对于下面的同步生成器,这会进入 finally。手动 next 则没有循环替你完成这个动作,需要在调用方自己的 finally 中明确关闭,才能保证提前结束时也配对清理。

用轨迹区分暂停与关闭

下面保存为 demo.mjs,用 Node.js 运行。open 与 close 只是数组里的标记,不会打开真实文件。生成器在进入时记录 open,在 finally 里记录 close,两个 yield 之间不做额外操作,让清理发生的准确时间可以直接观察。

第一组只取一个元素就 break。第二组手动读取后先检查轨迹,确认此时只有 open,再调用 return。第三组在尚未开始的生成器上直接 return,用来说明函数体根本没有进入时,体内的 finally 也没有运行。

JavaScript 生成器提前退出:for...of 的 break 怎样触发 finally 清理

AI概念示意图:以抽象物件说明本文主题,不代表真实界面或运行结果。

import assert from 'node:assert/strict';
function* records(trace) {
  trace.push('open');
  try {
    yield 'first';
    yield 'second';
  } finally {
    trace.push('close');
  }
}

const loopTrace = [];
for (const item of records(loopTrace)) {
  loopTrace.push(item);
  break;
}
assert.deepEqual(loopTrace, ['open', 'first', 'close']);
console.log(loopTrace.join(','));

const manualTrace = [];
const iterator = records(manualTrace);
assert.deepEqual(iterator.next(), {value: 'first', done: false});
assert.deepEqual(manualTrace, ['open']);
console.log(`paused=${manualTrace.join(',')}`);
try {
  // The consumer decides it has enough data.
} finally {
  assert.deepEqual(iterator.return('stop'), {value: 'stop', done: true});
}
assert.deepEqual(manualTrace, ['open', 'close']);
assert.deepEqual(iterator.next(), {value: undefined, done: true});
console.log(`closed=${manualTrace.join(',')}`);

const unusedTrace = [];
const unused = records(unusedTrace);
assert.deepEqual(unused.return(), {value: undefined, done: true});
assert.deepEqual(unusedTrace, []);
console.log(`never-started=${unusedTrace.length}`);

把资源获取与清理放在同一段生命周期

第一行应为 open,first,close。接着是 paused=open、closed=open,close,最后是 never-started=0。它们分别表示自动关闭、手动暂停、显式关闭,以及尚未进入函数体就结束。done 为真之后再次 next,也不会重新启动这份生成器。

因此,资源若由生成器管理,最好在函数体开始执行时获取,并在对应的 try/finally 中释放。不要在外面先获取资源,再把释放责任完全交给一份可能从未启动的生成器,否则第三组展示的情况会让清理逻辑没有机会执行。

手动消费时,可以把整个读取流程放进调用方的 try/finally,退出时调用 iterator.return。这个写法针对提供该方法的生成器;一般迭代器未必实现 return,需要先了解接口契约。资源是否真正释放,也取决于实现有没有把释放动作写进去。

避免在清理段里再次暂停

生成器的 finally 本身也允许 yield。如果清理段再次产出值,return 可能返回 done:false,生成器仍未彻底结束。本例故意让 finally 只做同步清理,避免关闭过程被再次暂停。设计资源型迭代器时,应把这种行为明确排除或完整记录。

不要在 finally 中随意 return 一个新的值,它可能改变原先退出时携带的结果,也让错误来源更难理解。清理动作若可能失败,应事先决定怎样报告,避免把原来的业务异常完全遮住。对需要异步释放的资源,应设计相应的异步迭代接口。

本例讨论正常执行中的控制流程,不保证进程被强制终止后 finally 仍会运行,也不依赖垃圾回收替你及时调用关闭。资源有持久外部状态时,还需要独立的恢复策略;生成器清理只能覆盖代码实际获得执行机会的路径。

验收一个流式接口时,至少测试完整读完、取一条就退出、读取中抛错和从未启动四种情况。逐一检查获取与释放次数,才能知道调用方式改变后是否仍守住资源边界。只看最终拿到的数据正确,并不足以证明生命周期正确。

参考资料

  1. ECMAScript:Generator 对象与 return

  2. ECMAScript:IteratorClose

  3. ECMAScript:for-in 与 for-of 执行

文章版权声明:除非注明,否则均为云鹊BLOG原创文章,转载或复制请以超链接形式并注明出处。