Skip to content

第五章练习答案 ​

回调与错误 ​

  1. map 在自身返回前同步调用回调。回调描述参数的角色,异步描述执行时序,两者没有必然关系。
  2. 不能。未来回调抛错时注册函数的 try 已经退出。把捕获放在回调内,或把失败转换到调用方等待的 Promise 链。
  3. interval 不消费 async 回调返回的 Promise,也不等待它。回调第一次暂停后,下次计时器仍可启动新请求。要等待完成,使用有停止条件的串行异步循环。

让解析异常进入调用方等待的结果 ​

js
function readText() {
  return new Promise(resolve => setTimeout(() => resolve('{bad'), 0));
}
const parsed = readText().then(text => JSON.parse(text));
try { await parsed; }
catch (error) { console.log(error.name); } // SyntaxError

第一层只交付文本;解析发生在 then 处理器中,抛错由处理器规则转为 parsed 的拒绝,再由 await 在调用方抛出。链里没有游离 Promise,也不依赖原注册计时器处的 try 跨越时间边界。

实作:把对象方法的回调接口接起来 ​

js
function readJSON(reader) {
  return new Promise((resolve, reject) => {
    reader.read((error, text) => {
      if (error) { reject(error); return; }
      try { resolve(JSON.parse(text)); }
      catch (failure) { reject(failure); }
    });
  });
}
const good = { text: '{"n":2}', read(cb) { setTimeout(() => cb(null, this.text), 0); } };
console.assert((await readJSON(good)).n === 2);
const syncError = new Error('调用失败');
const callbackError = new Error('读取失败');
const cases = [
  [{ read() { throw syncError; } }, syncError],
  [{ read(cb) { setTimeout(() => cb(callbackError), 0); } }, callbackError]
];
for (const [reader, expected] of cases) {
  const result = await readJSON(reader).catch(error => error);
  console.assert(result === expected);
}
const invalid = { read(cb) { setTimeout(() => cb(null, '{bad'), 0); } };
console.assert(await readJSON(invalid).then(
  () => false,
  error => error instanceof SyntaxError
));
for (const [text, expected] of [['null', null], ['1', 1], ['true', true]]) {
  const reader = { read(cb) { cb(null, text); } };
  console.assert(Object.is(await readJSON(reader), expected));
}

reader.read 保留接收者。同步调用失败由 Promise 执行器捕获;未来读取错误由 reject 接入;解析异常在回调自己的 try 中捕获。这里约定读取是一次性回调,不把持续订阅包装成单个结果。

Promise 与事件循环 ​

  1. resolve 首次调用锁定了跟随关系;最终状态仍取决于 inner。后续 reject 无效,但 inner 未落定前外层仍 pending。
  2. 两个处理器是针对同一个上游结果的备选路径。success 抛错影响的是 then 新产生的下游 Promise,需后续 catch 捕获。
  3. 正常返回 99 会被忽略,原结果继续传递;抛错会以新错误拒绝下游。清理操作也可能改变最终失败原因。

两个返回值规则不能混用 ​

then 处理器的 return 参与新 Promise 的决议;thenable 的 then 方法必须调用传入的 resolve/reject,它自身 return 的值不作为决议值。下面不去 await 这个一直等待的 Promise,而是观察没有触发成功处理器:

js
let fulfilled = false;
const pending = Promise.resolve({ then() { return 7; } });
pending.then(() => { fulfilled = true; });
await Promise.resolve();
await Promise.resolve();
console.log(fulfilled); // false

本例的 pending 没有外部活动资源,在 Node 中不会仅因“还有一个 pending Promise”就保持进程存活。若 thenable 调用 resolve(7) 后再 throw,决议已锁定,后来的拒绝尝试不能覆盖成功。这与 finally 抛错改变下游新 Promise不同,应先确认正在决定哪个 Promise。

实作:修复没有返回的后续工作 ​

js
function parseLater(text) {
  return Promise.resolve(text).then(value => {
    return Promise.resolve(value).then(JSON.parse);
  });
}
console.assert((await parseLater('{"ok":true}')).ok === true);
const failure = await parseLater('{broken').catch(error => error);
console.assert(failure instanceof SyntaxError);
for (const [text, expected] of [['null', null], ['1', 1], ['true', true]]) {
  console.assert(Object.is(await parseLater(text), expected));
}

外层 return 把整条链交给调用方,处理器中的 return 把内层解析结果接到外层下游。少了后者,处理器正常返回 undefined,后续工作变成游离分支。这里只有一次转换,实际可以简化成 return Promise.resolve(text).then(JSON.parse),但修复时要先认清两层结果归属。

组合与取消 ​

  1. 不会。all 提前拒绝只结束结果等待,其他操作继续。需要取消时将信号传给各操作,并明确谁拥有控制器。
  2. abort 事件只发生一次;传入已经取消的信号以后再监听,不能补收过去的事件。主动检查可以立即拒绝。
  3. 取消是协作性的,也可能发生得太迟,不能保证撤销已到达结果或副作用。版本检查独立保证提交结果仍属于当前请求。

取消权属于操作拥有者 ​

signal 不能复位;独立的新操作应创建新 controller。接受 signal 的底层函数只观察约定,不应该掌握中止兄弟任务的 controller。两个消费者共享请求时,一位离开可能只解除自己的订阅;是否中止底层工作,由共享请求的拥有者根据剩余需求决定。

return await run(signal) 在超时包装中也有必要:finally 要等操作落定以后清理超时计时器。直接 return Promise 会先执行 finally,提前清除计时器。这个区别来自资源寿命,而不是“await 越多越保险”。

修复丢失增量的例子可以让各项返回独立结果,再合并:

js
const increments = await Promise.all([2, 3].map(async n => {
  await Promise.resolve();
  return n;
}));
const total = increments.reduce((sum, n) => sum + n, 0);
console.log(total); // 5

这避免了每个操作持有 total 的旧值。若操作必须实时影响同一资源,返回增量未必符合需求,就要选择串行或资源本身支持的并发控制。

实作:两个使用者各有自己的最新结果 ​

js
function createLatest(publish, reportError) {
  let version = 0;
  return async function run(load) {
    const mine = ++version;
    let value;
    try { value = await load(); }
    catch (error) {
      if (mine === version) reportError(error);
      return;
    }
    if (mine === version) publish(value);
  };
}
const seen = [], errors = [];
const run = createLatest(value => seen.push(value), error => errors.push(error));
let rejectOld;
const old = run(() => new Promise((resolve, reject) => { rejectOld = reject; }));
const otherSeen = [];
const other = createLatest(value => otherSeen.push(value), () => {});
let finishNew;
const fresh = run(() => new Promise(resolve => { finishNew = resolve; }));
await other(() => Promise.resolve('另一组'));
finishNew('新结果');
await fresh;
rejectOld(new Error('旧失败'));
await old;
console.assert(seen.join() === '新结果' && errors.length === 0);
console.assert(otherSeen.join() === '另一组');
await run(() => { throw new Error('当前失败'); });
console.assert(errors[0].message === '当前失败');

编号存在工厂闭包中;每次启动就更新,成功和失败各自检查归属。它不取消底层任务,只保护提交。publish/reportError 的异常仍通过 run 的 Promise 交给调用者,不能当成加载失败再吞掉,因此调用者仍需消费 run 的返回结果。

并发与调度 ​

  1. 不能。Promise 通常表示已经开始或已经结束的工作。限制启动必须推迟调用产生工作的函数。
  2. 最多 limit 项在运行,观察到失败后不再领取新任务;已经运行的任务不会自动取消。外层可能在它们收尾之前拒绝,因此调用方不能把拒绝等同于所有副作用停止。
  3. Promise 恢复仍在微任务检查点,持续追加微任务会推迟任务和渲染机会。要分片并选择能够让宿主处理其他工作的调度边界。

完整收尾会改变返回时间 ​

等待 allSettled 可以确保所有 worker 的 Promise 已落定,再统一报告失败;但慢任务或不响应取消的任务会延后函数返回。需要及时失败且完整清理,通常要让各 worker 支持取消,并在通知取消后等待其收尾。不能把一个 never-settling 操作强行视为已经释放。

若要求保留“最早观察到的失败”,在 worker catch 的同步部分记录一次错误和布尔标志。不要只以 firstError === undefined 判断是否已有错误,因为 JavaScript 允许 throw undefined。按 allSettled 数组顺序找首个 rejected,得到的则是输入位置最靠前的失败,这不一定是时间上的首次。

实作:收集每项结果而不提前终止 ​

js
async function mapLimitResults(items, limit, mapper) {
  if (!Number.isInteger(limit) || limit < 1) throw new RangeError('limit');
  const results = new Array(items.length);
  let next = 0;
  async function worker() {
    while (next < items.length) {
      const index = next++;
      try {
        results[index] = { status: 'fulfilled', value: await mapper(items[index], index) };
      } catch (reason) {
        results[index] = { status: 'rejected', reason };
      }
    }
  }
  await Promise.all(Array.from({ length: Math.min(limit, items.length) }, worker));
  return results;
}
let active = 0, peak = 0;
const results = await mapLimitResults([1, 2, 3], 2, async n => {
  active++; peak = Math.max(peak, active);
  try {
    await Promise.resolve();
    if (n === 2) throw new Error('坏输入');
    return n * 10;
  } finally { active--; }
});
console.assert(peak === 2 && active === 0);
console.assert(results[0].value === 10 && results[1].status === 'rejected');
console.assert(results[2].value === 30);
console.assert((await mapLimitResults([], 2, n => n)).length === 0);

失败转成该输入位置的结果,worker 因此能继续领取;最后等所有 worker 结束。限制仍在调用 mapper 的位置,而不是先启动全部再 allSettled。这个版本保留全部输入与结果,适合有限数组;持续来源需要下一章的拉取与背压。

用可控完成顺序验证旧结果保护 ​

js
(async () => {
  let version = 0;
  let finishOld;
  const shown = [];
  async function run(promise) {
    const mine = ++version;
    const value = await promise;
    if (mine === version) shown.push(value);
  }
  const old = run(new Promise(resolve => { finishOld = resolve; }));
  await run(Promise.resolve('新结果'));
  finishOld('旧结果');
  await old;
  console.log(shown.join(',')); // 新结果
})();

没有依赖网络速度或随机计时。第二项先完成,第一项后完成,版本检查明确拒绝旧结果提交。删掉检查就会看到“新结果,旧结果”,直接重现覆盖风险。

用心记录每一步成长