速查与练习 — 语法卡片、五大场景、五道习题¶
这一章是全书机制的压缩卡片: 第一部分是语法速查(每种写法一行标准形状); 第二部分是五大场景的对照实现(同一个问题,Promise 版与 async/await 版并排); 第三部分是五道练习与解法。 用法不是通读,是当字典查——写完一段代码,回来对一眼"标准形状"。
1. 这一章讲什么¶
原书附录分 A/B/C 三份,角色各不相同:
| 部分 | 内容 | 正确用法 |
|---|---|---|
| 附录 A | Promise / async-await 语法速查 | 忘了写法时对一眼 |
| 附录 B | 五大场景的两种实现 | 场景对号,抄骨架改细节 |
| 附录 C | 五道练习与双解 | 自测:先自己写,再对解法 |
2. 顶层全景:场景到工具的跳转表¶
我要…… 查哪里
串行 API 调用 附录 B 场景 1(正文第 06 章 3.2)
并行 API 调用 附录 B 场景 2(正文第 04 章 3.1)
请求重试 附录 B 场景 3 + 练习 5(正文第 06 章 4)
请求可取消 附录 B 场景 4(正文第 09 章 5.3)
搜索防抖 附录 B 场景 5(正文第 12 章 5)
给操作加超时 练习 4(正文第 04 章 3.2 的完整版)
3. 附录 A:语法速查卡¶
每张卡给"标准形状",解释见正文对应章。
创建与消费一张票(第 03 章)1:
const myPromise = new Promise((resolve, reject) => {
if (/* 成功 */) resolve(result);
else reject(error);
});
myPromise
.then(result => { /* 成功 */ })
.catch(error => { /* 失败 */ })
.finally(() => { /* 收尾,成败都跑 */ });
链式(第 03 章 5):回调里 return 下一个值或下一张票1。
四路并发(第 04 章):Promise.all(全成/一拒全拒)、Promise.race(先落定)、Promise.allSettled(个个交代)、Promise.any(第一个成功)1。
async 函数的标准形状(第 05 章)2:
async function myAsyncFunction() {
try {
const r1 = await asyncOperation1();
const r2 = await asyncOperation2(r1);
return r2;
} catch (error) { console.error(error); }
}
const myAsyncArrow = async () => { /* ... */ }; // 箭头函数同样适用
// 并行:数组里先全部发起,再一把 await
const [r1, r2, r3] = await Promise.all([op1(), op2(), op3()]);
3.1 速查卡的三个使用场景¶
附录 A 的卡片薄,但它的使用场景值得说透:
写代码时:对形状。 忘了 Promise.all 的结果是个数组还是别的,翻一眼 3 节的形状行;不确定 async 箭头怎么写,看"异步函数的标准形状"那张。卡片不教为什么,只保证写出来的形状不错——为什么永远回正文章节。
读代码时:认模式。 见到一段陌生代码,拿卡片逐行对:这里在造票,那里在链,最后挂了 catch——三秒之内能说出这段代码在"卡片"的哪一格,就具备了读任何异步代码的基本功。卡片是最好的"模式识别练习册",附录 B 的五段代码都适合拿来做这个练习。
评审时:查缺漏。 一段取数代码摆在面前,对着卡片逐项问:ok 检查了吗?catch 挂了吗?并行的请求是不是漏了 all?卡片的覆盖面就是评审清单的覆盖面——这也解释了为什么附录 A 只收"标准形状":它们恰好是每个异步代码都该有的最小完整结构。
4. 附录 B:五大场景对照¶
每个场景两份实现并排,差异点就是两种写法的风格差。
场景一:串行 API 调用(后一步依赖前一步)¶
骨架:取用户 → 取帖子(用 userData.username)→ 取评论(用 posts[0].id)。Promise 版是 then 链里 return 下一张票;async 版是三个 await 直线3。判据见第 06 章 3.2:依赖存在,只能串。
场景二:并行 API 调用¶
Promise 版先造好三张票存进变量(发出请求与挂回调分开),再 Promise.all([p1, p2, p3]);async 版直接 await Promise.all([...])4。细节:票在数组字面量里就已发起——这是"并行要显式说"(第 05 章 6)的卡片化。
场景三:重试机制¶
Promise 版用闭包计数(attempts)递归;async 版用 while (attempts < maxAttempts) 循环 + await delay5。与第 06 章 4 的版本相比,这里是固定间隔(delay 不变),不带退避;升级路径就是把 delay 换成 2^i × 1000。
场景四:可取消操作(本附录的第一处独有实现)¶
正文只在第 09 章 5.3 给过取消的语法片段,这里给完整封装——把"票"和"取消柄"捆成一个对象返回6:
function cancellableApiCall(url) {
const controller = new AbortController();
const signal = controller.signal;
const promise = fetch(url, { signal })
.then(response => response.json())
.catch(error => {
if (error.name === 'AbortError') console.log('Fetch aborted');
else throw error;
});
return {
promise,
cancel: () => controller.abort() // 调用方手里的取消按钮
};
}
// 用法
const { promise, cancel } = cancellableApiCall('/api/data');
cancel(); // 随时可按
这个形状(票 + 柄)是取消的设计模式:取消权作为一等返回值交给调用方,而不是藏在内部。
场景五:防抖的 Promise 版¶
正文(第 00 章)的防抖返回的是 void,不好接 async;这里的版本让防抖函数返回 Promise,可以 await 它的最终结果7:
function debounce(func, delay) {
let timeoutId;
return function (...args) {
return new Promise((resolve, reject) => {
if (timeoutId) clearTimeout(timeoutId); // 停手就作废上一张票
timeoutId = setTimeout(() => {
func.apply(this, args).then(resolve).catch(reject);
}, delay);
});
};
}
const debouncedSearch = debounce(searchApi, 300);
每次输入,上一张还没兑现的票就被作废(clearTimeout),只有停手 300ms 后的那张真正发出——防抖的"只认最后一次",用票的生死来写。
4.1 场景一双解对照:串行 API 调用¶
需求:取用户 → 用他的用户名取帖子 → 用第一篇帖子的 ID 取评论11。两版并排:
// Promise 版:return 下一张票
function sequentialApiCalls() {
return fetchUserData(userId)
.then(userData => fetchUserPosts(userData.username))
.then(posts => fetchPostComments(posts[0].id))
.then(comments => console.log('Comments:', comments))
.catch(error => console.error('Error in sequential calls:', error));
}
// Async/Await 版:三行直线
async function sequentialApiCalls() {
try {
const userData = await fetchUserData(userId);
const posts = await fetchUserPosts(userData.username);
const comments = await fetchPostComments(posts[0].id);
console.log('Comments:', comments);
} catch (error) {
console.error('Error in sequential calls:', error);
}
}
对照点:链版的"顺序"由三次 return 承担,await 版的"顺序"由三行的上下承担;错误都汇到一处。差别在第二步的输入——链版靠回调参数流下去(userData 是上一个 then 的参数),await 版靠变量名存下来。哪个更好读见仁见智,但 await 版在"中间插一步"时改起来更轻(第 05 章 4 节的对照结论)。
4.2 场景二双解对照:并行 API 调用¶
需求:资料、全部帖子、全部评论互不依赖,齐了再用12:
// Promise 版:先存票再捆
function parallelApiCalls() {
const userPromise = fetchUserData(userId);
const postsPromise = fetchAllPosts();
const commentsPromise = fetchAllComments();
return Promise.all([userPromise, postsPromise, commentsPromise])
.then(([userData, posts, comments]) => {
console.log('All data fetched:', { userData, posts, comments });
})
.catch(error => console.error('Error in parallel calls:', error));
}
// Async/Await 版:发起与捆包写在一行
async function parallelApiCalls() {
try {
const [userData, posts, comments] = await Promise.all([
fetchUserData(userId),
fetchAllPosts(),
fetchAllComments()
]);
console.log('All data fetched:', { userData, posts, comments });
} catch (error) {
console.error('Error in parallel calls:', error);
}
}
Promise 版"先存票再捆"的写法有一个教学意义:三张票是分别造出来、各自在途的,Promise.all 只是把它们捆成一张——这能看见"并行"发生在数组之前(第 04 章 3.8 的前提其二)。await 版把"造票"与"捆包"合并进一行,更短,但看不见票各自的轨迹。排障需要后者时,临时拆成前者。
4.3 场景三双解对照:重试机制¶
需求:不稳的接口,失败再试,最多 N 次13。两版的控制流形状完全不同,值得并读:
// Promise 版:闭包计数 + 内层函数递归
function retryApiCall(maxAttempts = 3, delay = 1000) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
attempts++;
unreliableApiCall()
.then(resolve) // 成功:把值递出外层的票
.catch(error => {
if (attempts < maxAttempts) {
console.log(`Attempt ${attempts} failed. Retrying...`);
setTimeout(attempt, delay); // 失败后:等一等,再调自己
} else {
reject(new Error(`Failed after ${maxAttempts} attempts`));
}
});
}
attempt();
});
}
// Async/Await 版:while 循环
async function retryApiCall(maxAttempts = 3, delay = 1000) {
let attempts = 0;
while (attempts < maxAttempts) {
try {
return await unreliableApiCall(); // 成功:直接 return
} catch (error) {
attempts++;
if (attempts === maxAttempts) {
throw new Error(`Failed after ${maxAttempts} attempts`);
}
console.log(`Attempt ${attempts} failed. Retrying...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
两版是同一个逻辑的两种语法灵魂:Promise 版把"重试"写成一个会自己递归的内层函数(attempt 调 attempt),计数靠外层闭包;async 版把它写成一个会循环的函数体,计数靠普通变量。前者是"回调思维穿上 Promise 外衣",后者是"直线思维"。今天写新代码用后者;读到老代码里的前者,认出它是同一件事即可。
4.4 场景五的防抖双解:作废旧票的两种方式¶
场景五(防抖)的 Promise 版在正文 §4 已给,这里补它的对照说明14:第 00 章的防抖返回 void——调用方发了就不管了;这里的版本返回 Promise——调用方可以 await"用户终于停手后的那次搜索结果"。代价是语义变化:快速连击时,作废的旧票既不兑现也不拒绝(永远悬挂)。介意的话,在 clearTimeout 前补一句"把上一张票的 reject 调掉",改造一行即可,但多数搜索场景不需要——没人会 await 一个已经被新输入作废的结果。
5. 附录 C:五道练习¶
五道题的难度台阶,双解( Promise 版/async 版)的价值在对照:同一个控制流,两种写法的落点不同。
| 练习 | 练什么 | 解法要点 |
|---|---|---|
| 1 随机延迟问候 | 造票+setTimeout | delay 取 1-5 秒随机;await 版用 sleep 后 return |
| 2 并行取数合并 | Promise.all | 两请求同时发,结果合成 |
| 3 三步串行+逐步错误 | 链上逐步 throw | 每步查 ok、各自报错文案,catch 带路径信息 |
| 4 超时包装 withTimeout | Promise.race | 操作与计时器赛跑;操作先到要清掉计时器防误报 |
| 5 指数退避重试 | 退避公式 | delay = baseDelay × 2^(attempts-1) |
练习四:withTimeout —— 给任何异步操作加时限¶
这是全附录最值钱的解法,正文只给过雏形(第 04 章 3.2 的超时赛跑)。完整版的关键细节:操作先完成时必须清掉计时器,否则计时器到点照样引爆8:
function withTimeout(asyncOperation, timeoutMs) {
return new Promise((resolve, reject) => {
const timeoutId = setTimeout(() => reject(new Error('Operation timed out')), timeoutMs);
asyncOperation()
.then(result => { clearTimeout(timeoutId); resolve(result); }) // 赢了也要拆弹
.catch(error => { clearTimeout(timeoutId); reject(error); });
});
}
// 用法:3 秒的操作给 5 秒时限 → 成功;给 2 秒时限 → 报 Operation timed out
withTimeout(slowOperation, 5000).then(...); // 成功
withTimeout(slowOperation, 2000).then(...); // 超时
async 版同样用 race,更短:await Promise.race([asyncOperation(), timeoutPromise])9——但注意 race 版没有 clearTimeout,计时器到点仍会引爆(只是没人听了);要让计时器真正停下,用完整版。 这个差别正是"race 是假装超时,abort 是真取消"(第 09 章 5.3)的又一个注脚。
练习五:指数退避的公式版¶
解法把退避写成一行公式:delay = baseDelay * 2^(attempts - 1)——第 1 次失败后等 baseDelay,第 2 次等 2×,第 3 次等 4×10。与第 06 章 4.3 一致;缺口也一致:无抖动、无总时长上限。
5.1 练习一、二详解:造票与并行合并¶
练习一(随机延迟问候)要一个函数:传入名字,随机等 1-5 秒,回来一句问候15。两版:
// Promise 版:等待写进造票里
function delayedGreeting(name) {
return new Promise((resolve) => {
const delay = Math.floor(Math.random() * 4000) + 1000; // 1000-4999ms
setTimeout(() => {
resolve(`Hello, ${name}! Sorry for the ${delay/1000} second delay.`);
}, delay);
});
}
// Async/Await 版:等待写在函数体里
async function delayedGreeting(name) {
const delay = Math.floor(Math.random() * 4000) + 1000;
await new Promise(resolve => setTimeout(resolve, delay));
return `Hello, ${name}! Sorry for the ${delay/1000} second delay.`;
}
这个练习练的是"等待"本身的两种写法:new Promise 包 setTimeout(造票时把等待装进去)与 await 一个 delay(等待写在直线里)。注意随机数那行两版一字不差——随机逻辑与异步写法完全无关。
练习二(并行取数合并)要:同时取用户资料与帖子,合成一个对象16。它的关键是"两个 URL 不同(资料在 /users/{id},帖子在 /users/{id}/posts)但依赖同一个 id"——第一步拿到 id 之后,两路才能发出。Promise 版用"先存票再 all"(与场景二同款),async 版在数组里同时写两个 fetch。做完这个练习,第 03 章 5.12 的链内并行、第 05 章的 all 组合就都串起来了。
5.2 练习三详解:逐步报错的串行链¶
练习三要求三步串行且每步有各自的报错文案17:
async function sequentialApiCalls(initialId) {
try {
const response1 = await fetch(`/api/step1/${initialId}`);
if (!response1.ok) throw new Error('Error in step 1');
const data1 = await response1.json();
const response2 = await fetch(`/api/step2/${data1.nextId}`);
if (!response2.ok) throw new Error('Error in step 2');
const data2 = await response2.json();
const response3 = await fetch(`/api/step3/${data2.finalId}`);
if (!response3.ok) throw new Error('Error in step 3');
const finalData = await response3.json();
return finalData;
} catch (error) {
console.error('Error in sequential calls:', error.message); // 哪一步,看 message
throw error;
}
}
三步的形状完全重复(fetch → 查 ok → json),这正是"每步一个取数函数"重构(第 10 章 3.2 的三分法)要消灭的重复;但这个练习故意不重构,因为它练的是错误文案与步骤的绑定——message 里带着"step 1/2/3",catch 打印出来,排障时一眼定位。顺带注意 catch 里的 throw error:练习的测试要用 rejects 断言(见下),所以错误必须继续递出去。
5.2a 练习四详解:withTimeout 的双解与"哑弹"细节¶
练习四(给任意操作加时限)在正文 §5 给了 Promise 版的完整实现与 race 版的对照,这里补 async 版的全貌19:
async function withTimeout(asyncOperation, timeoutMs) {
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Operation timed out')), timeoutMs)
);
try {
return await Promise.race([asyncOperation(), timeoutPromise]);
} catch (error) {
throw error;
}
}
三个解读点。其一,timeoutPromise 的构造器只接 reject 不接 resolve——下划线是"这个参数用不上"的习惯记号,它一旦先到,比赛就结束了。其二,asyncOperation() 在数组里被调用了(第 04 章 3.8 的前提:括号不能省)。其三,竞胜之后超时计时器没有被拆掉——这是 race 的天性:它只宣布胜负,不管善后。Promise 版用 clearTimeout 拆了弹,这个 async 版没有——这正是正文那句"要计时器真正停下,用完整版"的代码证据。两个解法都是对的,选哪个取决于你在不在乎那颗哑弹。
5.3 练习五详解:指数退避的完整双解与常见错法¶
练习五是不稳接口 + 指数退避 + 明确总次数,双解并读18:
// Promise 版:attempt 递归
function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
return new Promise((resolve, reject) => {
let attempts = 0;
function attempt() {
operation()
.then(resolve)
.catch(error => {
attempts++;
if (attempts >= maxRetries) {
reject(new Error(`Operation failed after ${maxRetries} attempts`));
} else {
const delay = baseDelay * Math.pow(2, attempts - 1); // 1×, 2×, 4×
console.log(`Attempt ${attempts} failed. Retrying in ${delay}ms...`);
setTimeout(attempt, delay);
}
});
}
attempt();
});
}
// Async/Await 版:for + try/catch
async function retryWithBackoff(operation, maxRetries = 3, baseDelay = 1000) {
for (let attempts = 1; attempts <= maxRetries; attempts++) {
try {
return await operation();
} catch (error) {
if (attempts === maxRetries) {
throw new Error(`Operation failed after ${maxRetries} attempts`);
}
const delay = baseDelay * Math.pow(2, attempts - 1);
console.log(`Attempt ${attempts} failed. Retrying in ${delay}ms...`);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
}
三个常见错法,做过才知道疼:
- 退避公式的 off-by-one:写成
Math.pow(2, attempts)会让第一次重试等 2 秒而不是 1 秒;写成attempts - 1才对应"第 1 次失败后等 1×基底"。不确定时把 attempts=1 代进去手算一遍。 - 全败时丢原始错误:两个版本都在最后一次抛了新的 Error,原始的失败原因(网络错误?500?)被包在里面没带出来。生产中要把原始错误挂进新错误的 cause(第 09 章 5.4 的 Error Cause 就是干这个的)。
- 非幂等操作直接套:练习的 operation 是"查询"性质的,重试安全;换成"下单",这套重试可能在第一次其实成功、只是响应丢了的情况下重复扣款——第 06 章 4.5 判断块提过,这里再见一次。
6. 这一章(附录)的判断与证据¶
附录没有独立判断,但它的编排本身是个判断:五种场景选的都是"每天都会遇到"的(取数、重试、取消、防抖、超时),而不是技术上最有趣的——服务实战的取向与全书一致。
证据形态:全部是自包含代码,除可取消与超时两例外都不依赖外部服务,可直接贴进控制台验证。
7. 边界与局限¶
- 速查卡只收 Promise/async-await,不含回调与 Observable(后两者分别由第 02、09 章承载);
- 场景与练习的 Promise 版大量使用
new Promise手工包装,其中一部分(如重试)在今天可以直接用 async 版取代——双解并存是教学对照,不是风格建议; - 防抖 Promise 版有个未言明的边界:连续快速输入时,作废的旧票会 reject 吗?原实现让旧票永远悬挂(既不 resolve 也不 reject)——await 它们的人会一直等,实战中常需要补一个"作废即拒"的分支;
- 没有 Promise.withResolvers 这类新语法糖(ES2024),手写 new Promise 仍是书中默认。
8. 可带走的¶
- 速查卡的正确用法:写完对一眼,不背;场景的查法是"需求 → 附录 B 场景号 → 正文章节";
- 可取消操作的标准形状:返回 { promise, cancel } 对象,取消权交给调用方;
- withTimeout 的完整版要 clearTimeout——race 版留了哑弹,计时器没真停下;
- 防抖 Promise 版用"作废上一张票"实现"只认最后一次";
- 练习四、五合起来就是生产级重试的骨架:退避公式 + 超时 + (再补上第 06 章的抖动与分诊);
- 双解对照读法:先盖住一种解法,用另一种写完,再对差异——差异处就是两种写法的性格。
9. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| 附录 A 速查 | Appendices | text/16-fm-appendices.txt:7(搜「Quick Reference」) · text/16-fm-appendices.txt:16(搜「new Promise」) · text/16-fm-appendices.txt:70(搜「async function」) |
| 场景一:串行 | Appendices | text/16-fm-appendices.txt:121(搜「Sequential API Calls」) |
| 场景二:并行 | Appendices | text/16-fm-appendices.txt:156(搜「Parallel API Calls」) |
| 场景三:重试 | Appendices | text/16-fm-appendices.txt:191(搜「Retry Mechanism」) |
| 场景四:可取消 | Appendices | text/16-fm-appendices.txt:237(搜「Cancellable」) · text/16-fm-appendices.txt:259(搜「cancel」) |
| 场景五:防抖 Promise 版 | Appendices | text/16-fm-appendices.txt:269(搜「Debouncing」) |
| 练习 1/2 | Appendices | text/16-fm-appendices.txt:309(搜「Delayed Greeting」) · text/16-fm-appendices.txt:349(搜「Parallel Data」) |
| 练习 3 | Appendices | text/16-fm-appendices.txt:406(搜「Sequential API Calls」) |
| 练习 4:withTimeout | Appendices | text/16-fm-appendices.txt:483(搜「Timeout」) · text/16-fm-appendices.txt:497(搜「clearTimeout」) |
| 练习 5:指数退避 | Appendices | text/16-fm-appendices.txt:554(搜「Exponential Backoff」) · text/16-fm-appendices.txt:572(搜「Math.pow」) |
(附录的正确打开方式:不读,查;查的时候记得每条都有正文章节在讲为什么。)
-
出处:「Appendices」附录 A(
text/16-fm-appendices.txt:16,搜「new Promise」;:26,搜「then」;:48,搜「Promise.all」;:56,搜「Promise.race」)。创建/消费/链式/四路并发的卡片形状照原书。 ↩↩↩ -
出处:同附录 A(
text/16-fm-appendices.txt:70,搜「async function」;:95,搜「Promise.all」)。async 函数声明、箭头形式、并行形状照原书。 ↩ -
出处:「Appendices」附录 B 场景 1(
text/16-fm-appendices.txt:127,搜「sequentialApiCalls」;:145,搜「async function sequentialApiCalls」)。双版骨架照原书。 ↩ -
出处:附录 B 场景 2(
text/16-fm-appendices.txt:162,搜「parallelApiCalls」)。先存票再 all 的顺序照原书。 ↩ -
出处:附录 B 场景 3(
text/16-fm-appendices.txt:197,搜「retryApiCall」;:221,搜「while」)。闭包递归版与 while 循环版照原书。 ↩ -
出处:附录 B 场景 4(
text/16-fm-appendices.txt:243,搜「cancellableApiCall」;:257,搜「cancel」)。{ promise, cancel } 对象与 AbortError 分支照原书。 ↩ -
出处:附录 B 场景 5(
text/16-fm-appendices.txt:275,搜「debounce」;:280,搜「clearTimeout」)。返回 Promise 的防抖与作废上一张票照原书。 ↩ -
出处:附录 C 练习 4(
text/16-fm-appendices.txt:489,搜「withTimeout」;:497,搜「clearTimeout」)。成功/失败两条路都 clearTimeout 的完整版照原书;用法中 5 秒时限成功、2 秒时限超时照原书。 ↩ -
出处:附录 C 练习 4 的 async 解法(
text/16-fm-appendices.txt:523,搜「timeoutPromise」;:528,搜「Promise.race」)。 ↩ -
出处:附录 C 练习 5(
text/16-fm-appendices.txt:560,搜「retryWithBackoff」;:572,搜「Math.pow」)。退避公式与 5 次 1000ms 基底的用法照原书。 ↩ -
出处:「Appendices」附录 B 场景 1(
text/16-fm-appendices.txt:127,搜「sequentialApiCalls」;:145,搜「async function sequentialApiCalls」)。两版对照照原书。 ↩ -
出处:「Appendices」附录 B 场景 2(
text/16-fm-appendices.txt:162,搜「parallelApiCalls」;:178,搜「async function parallelApiCalls」)。先存票再捆的写法照原书。 ↩ -
出处:「Appendices」附录 B 场景 3(
text/16-fm-appendices.txt:197,搜「retryApiCall」;:221,搜「while」)。闭包递归版与 while 版照原书。 ↩ -
出处:「Appendices」附录 B 场景 5(
text/16-fm-appendices.txt:275,搜「debounce」)。返回 Promise 的防抖照原书。 ↩ -
出处:「Appendices」附录 C 练习 1(
text/16-fm-appendices.txt:315,搜「delayedGreeting」;:331,搜「async function delayedGreeting」)。两版与随机延迟的取法照原书。 ↩ -
出处:「Appendices」附录 C 练习 2(
text/16-fm-appendices.txt:355,搜「fetchUserDataAndPosts」;:378,搜「async function fetchUserDataAndPosts」)。 ↩ -
出处:「Appendices」附录 C 练习 3(
text/16-fm-appendices.txt:412,搜「sequentialApiCalls」;:449,搜「async function sequentialApiCalls」)。逐步报错与 catch 再抛照原书。 ↩ -
出处:「Appendices」附录 C 练习 5(
text/16-fm-appendices.txt:560,搜「retryWithBackoff」;:600,搜「async function retryWithBackoff」)。双解与退避公式照原书。 ↩ -
出处:「Appendices」附录 C 练习 4 的 async 解法(
text/16-fm-appendices.txt:522,搜「withTimeout」;:528,搜「Promise.race」)。 ↩