Skip to content

第六章练习答案 ​

迭代器与生成器 ​

  1. steps 每次调用 Symbol.iterator 都创建新的状态 n;同一个生成器对象保留同一进度,结束后 next 继续报告 done。想重新遍历,应再次调用生成器函数取得新对象。
  2. for...of 在 done 为 true 时结束,不把终止 value 送进循环体。手动 next 可以观察它,yield* 表达式也能接收被委托生成器的终止值。
  3. 不保证。丢弃引用不是协议关闭。应由消费者明确 return 或通过能执行 IteratorClose 的结构提前退出;finally 还需要生成器已经开始执行。

关闭请求与完成状态要分别观察 ​

finally 中的 yield 会暂停关闭过程,return 的结果可能是 done:false。消费者若已经决定退出,不保证再 next 一次,所以资源释放不能依赖额外一次驱动。通常把清理做成不再 yield 的同步操作;异步清理由 async generator 内部 await,再让调用方等待 return 的 Promise。

IteratorClose 还有异常优先级:循环体 throw 已存在时保留原处理错误;break 关闭时 return 抛错则会把清理错误向外报告。普通 finally 的覆盖规则不能代替这个协议算法。

实作:实现惰性的筛选并验证关闭 ​

js
function* select(source, predicate) {
  for (const value of source) {
    if (predicate(value)) yield value;
  }
}
let produced = 0, closed = false;
function* source() {
  try {
    for (const value of [1, 2, 3, 4]) { produced++; yield value; }
  } finally { closed = true; }
}
for (const value of select(source(), n => n % 2 === 0)) {
  console.assert(value === 2);
  break;
}
console.assert(produced === 2 && closed);
console.assert([...select([1, 2, 3, 4], n => n > 2)].join() === '3,4');
console.assert([...select([1, 3], n => n % 2 === 0)].length === 0);

外层消费者 break 请求关闭 select;它在内部 for...of 的循环体暂停,退出该循环时会继续关闭 source,最终执行源的 finally。这个练习假设源正常响应 return 且清理不再 yield。若手动 next 后直接丢弃 select,便没有这条显式关闭路径。

异步迭代与流 ​

  1. 每次 next 才继续推进生成器。break 请求关闭,程序直接进入收尾,没有请求下一项。但这个结论不适用于生成器内部已经提前启动的后台任务。
  2. 不等于。releaseLock 只释放读取所有权;取消应通过 reader.cancel 等相应接口通知底层源,网络取消还要看具体源如何实现。
  3. 一个 UTF-8 字符可能跨块分布。每块使用独立 decoder 会把残缺序列视为错误。复用 decoder 并使用流模式,最后再完成解码,或者使用 TextDecoderStream。

解除所有权不是解除等待 ​

reader.releaseLock 让其他 reader 可以接管读取,不会自动撤销底层工作。async generator 的 return 请求则排在当前 next 后面;当前 next 还在 await,return 也可能继续等待。要及时结束,需要把取消信号传到真正等待的那一层,让它有办法完成或拒绝。

同样,writer.ready 表示背压解除,不是全部数据写完。等待某次 writer.write 的 Promise 可以确认该次底层写入契约完成;需要正常结束整条流,还要等待 close。分清这些 Promise 代表的完成条件,才能避免“已经可以继续写”被误用成“现在可以删除原文件”。

实作:处理跨块的 CRLF ​

js
function lines() {
  let pending = '';
  const clean = line => line.endsWith('\r') ? line.slice(0, -1) : line;
  return new TransformStream({
    transform(chunk, controller) {
      pending += chunk;
      let end;
      while ((end = pending.indexOf('\n')) !== -1) {
        controller.enqueue(clean(pending.slice(0, end)));
        pending = pending.slice(end + 1);
      }
    },
    flush(controller) {
      if (pending !== '') controller.enqueue(pending);
    }
  });
}
const output = [];
const input = new ReadableStream({
  start(controller) {
    for (const chunk of ['甲\r', '\n\r\n乙', '\r\n尾']) controller.enqueue(chunk);
    controller.close();
  }
});
await input.pipeThrough(lines()).pipeTo(new WritableStream({
  write(line) { output.push(line); }
}));
console.assert(JSON.stringify(output) === JSON.stringify(['甲', '', '乙', '尾']));

只在发现 LF 的完整行边界检查前一字符是否为 CR,因此 CR 和 LF 分属两个块也能匹配。未终止尾部没有 LF,末尾若有 CR 则仍作为正文保留;这避免误删单独的 CR。pending 仍需另设长度预算,UTF-8 解码也应在此转换之前完成。

二进制与共享内存 ​

  1. 大端把前一个字节当高位,小端把它当低位。0x12、0x34 分别解释为 0x1234 与 0x3412。协议必须事先约定,不能靠机器猜。
  2. subarray 的 buffer 与原数组相同,只改变视图区间;slice 分配并复制。判断是否共享可以比较 .buffer,但拥有同一个 buffer 不代表视图区间一定重叠。
  3. 两个线程可能都读到旧值 0,分别写入 1,最终少一次更新。Atomics.add 将对应位置的读改写作为原子操作,两个增量都会计入。多个原子操作组成的复杂事务仍需额外协议。

视图的范围与原子更新的返回值 ​

js
const bytes = new Uint8Array([0, 0, 4, 5, 0]);
const selected = bytes.subarray(2, 4);
const view = new DataView(selected.buffer, selected.byteOffset, selected.byteLength);
console.log(view.getUint16(0, false)); // 1029
try { view.getUint8(2); }
catch (error) { console.log(error.name); } // RangeError

偏移保证第0字节对应选中的起点,长度保证解析器不能越过当前消息边界。只传 buffer 就丢失了这两项限制。

compareExchange 返回修改前的值。返回值等于 expected,表示本次比较成功并已写入 replacement;返回其他值表示失败,不能把返回值当布尔值。本章把0约定为可领取时,成功恰好返回0,若写 if (Atomics.compareExchange(...)),反而会把成功当假值。

实作:解析带长度头的子视图 ​

js
function parsePacket(bytes) {
  if (!(bytes instanceof Uint8Array)) throw new TypeError('需要 Uint8Array');
  if (bytes.byteLength < 3) throw new RangeError('头部不完整');
  const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
  if (view.getUint8(0) !== 1) throw new Error('版本不支持');
  const length = view.getUint16(1, false);
  if (bytes.byteLength !== 3 + length) throw new RangeError('长度不匹配');
  return bytes.slice(3);
}
const storage = new Uint8Array([99, 1, 0, 2, 7, 8, 99]);
const payload = parsePacket(storage.subarray(1, 6));
storage[4] = 0;
console.assert(payload.join() === '7,8');
for (const data of [[1, 0], [2, 0, 0], [1, 0, 2, 7], [1, 0, 0, 7]]) {
  let rejected = false;
  try { parsePacket(new Uint8Array(data)); } catch { rejected = true; }
  console.assert(rejected);
}
console.assert(parsePacket(new Uint8Array([1, 0, 0])).length === 0);

先检查头长再读 DataView,避免拿越界异常代替协议判断;保留视图偏移与长度,避免读到外部存储。slice 明确选择复制。这个小解析器约定同一 realm、普通且未脱离的 Uint8Array,不接收并发修改的共享输入;流式拼包还需保存不完整数据,并区分“等待更多”与“包已无效”。

用跨块字符验证流式解码 ​

js
const bytes = new TextEncoder().encode('中');
const decoder = new TextDecoder('utf-8', { fatal: true });
const first = decoder.decode(bytes.subarray(0, 1), { stream: true });
const last = decoder.decode(bytes.subarray(1));
console.log(JSON.stringify(first), last); // "" 中

第一块不足以组成一个字符,decoder 暂存它并返回空串;第二次解码补齐字节并完成。若第一块不启用 stream,严格 decoder 当场抛错,说明分块边界必须由解码器状态衔接。

用心记录每一步成长