第五章练习答案
回调与错误
- map 在自身返回前同步调用回调。回调描述参数的角色,异步描述执行时序,两者没有必然关系。
- 不能。未来回调抛错时注册函数的 try 已经退出。把捕获放在回调内,或把失败转换到调用方等待的 Promise 链。
- interval 不消费 async 回调返回的 Promise,也不等待它。回调第一次暂停后,下次计时器仍可启动新请求。要等待完成,使用有停止条件的串行异步循环。
让解析异常进入调用方等待的结果
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 跨越时间边界。
实作:把对象方法的回调接口接起来
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 与事件循环
- resolve 首次调用锁定了跟随关系;最终状态仍取决于 inner。后续 reject 无效,但 inner 未落定前外层仍 pending。
- 两个处理器是针对同一个上游结果的备选路径。success 抛错影响的是 then 新产生的下游 Promise,需后续 catch 捕获。
- 正常返回 99 会被忽略,原结果继续传递;抛错会以新错误拒绝下游。清理操作也可能改变最终失败原因。
两个返回值规则不能混用
then 处理器的 return 参与新 Promise 的决议;thenable 的 then 方法必须调用传入的 resolve/reject,它自身 return 的值不作为决议值。下面不去 await 这个一直等待的 Promise,而是观察没有触发成功处理器:
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。
实作:修复没有返回的后续工作
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),但修复时要先认清两层结果归属。
组合与取消
- 不会。all 提前拒绝只结束结果等待,其他操作继续。需要取消时将信号传给各操作,并明确谁拥有控制器。
- abort 事件只发生一次;传入已经取消的信号以后再监听,不能补收过去的事件。主动检查可以立即拒绝。
- 取消是协作性的,也可能发生得太迟,不能保证撤销已到达结果或副作用。版本检查独立保证提交结果仍属于当前请求。
取消权属于操作拥有者
signal 不能复位;独立的新操作应创建新 controller。接受 signal 的底层函数只观察约定,不应该掌握中止兄弟任务的 controller。两个消费者共享请求时,一位离开可能只解除自己的订阅;是否中止底层工作,由共享请求的拥有者根据剩余需求决定。
return await run(signal) 在超时包装中也有必要:finally 要等操作落定以后清理超时计时器。直接 return Promise 会先执行 finally,提前清除计时器。这个区别来自资源寿命,而不是“await 越多越保险”。
修复丢失增量的例子可以让各项返回独立结果,再合并:
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 的旧值。若操作必须实时影响同一资源,返回增量未必符合需求,就要选择串行或资源本身支持的并发控制。
实作:两个使用者各有自己的最新结果
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 的返回结果。
并发与调度
- 不能。Promise 通常表示已经开始或已经结束的工作。限制启动必须推迟调用产生工作的函数。
- 最多 limit 项在运行,观察到失败后不再领取新任务;已经运行的任务不会自动取消。外层可能在它们收尾之前拒绝,因此调用方不能把拒绝等同于所有副作用停止。
- Promise 恢复仍在微任务检查点,持续追加微任务会推迟任务和渲染机会。要分片并选择能够让宿主处理其他工作的调度边界。
完整收尾会改变返回时间
等待 allSettled 可以确保所有 worker 的 Promise 已落定,再统一报告失败;但慢任务或不响应取消的任务会延后函数返回。需要及时失败且完整清理,通常要让各 worker 支持取消,并在通知取消后等待其收尾。不能把一个 never-settling 操作强行视为已经释放。
若要求保留“最早观察到的失败”,在 worker catch 的同步部分记录一次错误和布尔标志。不要只以 firstError === undefined 判断是否已有错误,因为 JavaScript 允许 throw undefined。按 allSettled 数组顺序找首个 rejected,得到的则是输入位置最靠前的失败,这不一定是时间上的首次。
实作:收集每项结果而不提前终止
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。这个版本保留全部输入与结果,适合有限数组;持续来源需要下一章的拉取与背压。
用可控完成顺序验证旧结果保护
(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(',')); // 新结果
})();没有依赖网络速度或随机计时。第二项先完成,第一项后完成,版本检查明确拒绝旧结果提交。删掉检查就会看到“新结果,旧结果”,直接重现覆盖风险。