异步:Promise、await、all 和 allSettledAsync: Promise, await, all and allSettled
Q2 整道题就是异步,GraphQL resolver 每一个都是 async。这一节把它们讲透。All of Q2 is async work, and every GraphQL resolver is async. This lesson covers all of it.
这一页有什么On this page7
- 01 Promise:一张「以后会给你结果」的凭据A Promise is a receipt for a result that arrives later
- 02 最关键的一个区分:函数,还是函数的返回值The key distinction: a function, or the value the function returns
- 03 Promise.all 和 Promise.allSettled:差别在「一个失败了怎么办」Promise.all and Promise.allSettled: they differ in what happens when one fails
- 04 并发上限的实现思路:共享一个游标的 workerHow to limit how many run at once: workers sharing one cursor
- 05 try/catch 包住 awaitWrapping await in try/catch
- 练习 · 动手做Practice
- 迁移模式Transfer
- 说清 Promise 的三种状态,以及 await 到底在等什么Explain the three states of a Promise, and what await is actually waiting for
- 分清 Promise.all 和 Promise.allSettled 的行为差别Tell apart how Promise.all and Promise.allSettled behave
- 知道「函数」和「函数的返回值」在异步里为什么必须分清Know why a function and the value a function returns must be kept apart in async code
- 会用 try/catch 包住 awaitWrap an await in try/catch
Q2 要你手写一个「allSettled + 并发上限」;Federation 的每个 resolver 都是 async 且要求 try/catch。这一节是两道题共同的地基。Q2 asks you to write allSettled behaviour with a limit on how many run at once. In Federation, every resolver is async and has to use try/catch. This lesson is the base both questions stand on.
react-notes-app/q2/taskRunner.tsQ2 的题面与要求都在文件顶部注释里The Q2 problem and its requirements are in the comment at the top of the file
react-notes-app/q2/taskRunner.tsreact-notes-app/q2/demo.ts验证台:打印实时并发数A test bench that prints the live concurrency count
react-notes-app/q2/demo.tsPromise:一张「以后会给你结果」的凭据A Promise is a receipt for a result that arrives later
它有三种状态,而且只会变一次。It has three states, and it settles only once.
Promise 代表一件「现在还没完成、以后会有结果」的事。 三种状态:
- pending —— 还在做
- fulfilled —— 成功了,带一个值
- rejected —— 失败了,带一个原因
一旦从 pending 变成后两者之一,就永久定下来,不会再变。
await 的作用是:「在这里等着,直到这个 Promise 定下来」。 成功就把值交给你,失败就抛出异常(所以要用try/catch 接)。await 只能写在async 函数里。
A Promise stands for something that is not finished yet but will have a result later. Three states:
- pending — still working
- fulfilled — succeeded, carries a value
- rejected — failed, carries a reason
Once it moves out of pending into one of the other two, it is settled for good and never changes again.
What await does is: “stand here until this Promise settles”. On success it hands you the value; on failure it throws (which is why you catch it with try/catch). await only works inside an async function.
graphql-federation-practice/node-subgraph/src/dataSources/orderDataSource.js最关键的一个区分:函数,还是函数的返回值The key distinction: a function, or the value the function returns
Q2 整道题都建立在这个区分上。The whole of Q2 rests on this distinction.
看 Q2 的类型定义:
Task<T> 是 一个函数: () => Promise<T>。 不是 Promise,是「调用之后才产生 Promise 的函数」。
这个区分为什么是整道题的关键?因为Promise 一旦创建就开始跑了, 没法暂停。如果 tasks 是一堆 Promise, 那它们在你拿到手之前就已经全部并发跑起来了,「并发上限」根本无从谈起。
正因为它们是函数,你才能控制「什么时候调用」—— 先调两个,等其中一个完了,再调第三个。这就是并发节流的全部原理。
Look at the Q2 type definitions:
Task<T> is a function: () => Promise<T>. Not a Promise — a function that only produces a Promise once you call it.
Why is that distinction the whole question? Because a Promise starts running the instant it is created, and there is no pause button. If tasks were a pile of Promises, they would all be running concurrently before you ever got your hands on them, and a concurrency cap would mean nothing.
Precisely because they are functions, you get to control when they are called — call two, wait for one to finish, then call the third. That is the entire principle behind concurrency throttling.
react-notes-app/q2/taskRunner.tsPromise.all 和 Promise.allSettled:差别在「一个失败了怎么办」Promise.all and Promise.allSettled: they differ in what happens when one fails
Promise.all | Promise.allSettled | |
|---|---|---|
| 全部成功 | 返回值数组 [v1, v2] | 返回 [{status:"fulfilled",value}, ...] |
| 有一个失败 | 立刻整体 reject,其他结果全丢 | 照样等全部结束,失败那个记成 rejected |
| 顺序 | 与输入一致 | 与输入一致 |
两个都保证顺序与输入一致 —— 哪个先完成不影响结果数组的位置。 这一点很多人以为是「谁先完成谁在前」,是错的。
Q2 的要求原文是「The runner NEVER throws, even if some tasks reject」, 也就是 allSettled 的语义,再加一个并发上限。 而 Promise.all 在两个考试里也真实出现了 —— subgraph 的 DataLoader 批量函数就用它同时取多个订单。
Promise.all | Promise.allSettled | |
|---|---|---|
| All succeed | An array of values [v1, v2] | Returns [{status:"fulfilled",value}, ...] |
| One fails | Rejects as a whole, immediately, and the other results are lost | Still waits for all of them; the failed one is recorded as rejected |
| Order | Matches the input | Matches the input |
Both of them guarantee the order matches the input — which one finishes first has no bearing on its slot in the result array. Plenty of people assume it is first-to-finish-comes-first. It is not.
The Q2 requirement reads “The runner NEVER throws, even if some tasks reject”, which is exactly allSettled semantics plus a concurrency cap. And Promise.all genuinely shows up in both exams too — the subgraph’s DataLoader batch function uses it to fetch several orders at once.
graphql-federation-practice/node-subgraph/src/resolvers/orderResolvers.js并发上限的实现思路:共享一个游标的 workerHow to limit how many run at once: workers sharing one cursor
别想复杂了。就是「开 limit 个工人,一起从同一个待办队列里抢活」。It is simpler than it sounds. Start limit workers, and let them all take the next job from the same queue.
想象 6 个任务,并发上限 2。错误的思路是「切成 3 批,每批 2 个」—— 那样每批都要等最慢的那个,浪费。
正确的思路是 worker pool(工人池):
- 准备一个共享游标
nextIndex = 0。 - 启动
limit个 worker,每个都是一个 async 函数。 - 每个 worker 循环:抢走当前游标指向的任务(游标 +1)→ 跑它 → 结果写回
results[i]→ 回到循环开头再抢下一个。 - 队列空了,worker 自然退出。
await Promise.all(workers)等所有 worker 收工。
同时在跑的永远只有 limit 个 worker,所以并发数天然不会超。 结果按 results[i] 的下标写回,所以顺序自动是对的, 不需要额外排序。
实测这个思路在项目里的输出(npm run q2):running now 始终 ≤ 2,最终 6 条结果顺序与输入一致, 第 3 个任务以 rejected 出现。完整实现在 React 那门课的 Q2 那一节。
Picture 6 tasks with a concurrency cap of 2. The wrong idea is “cut it into 3 batches of 2” — every batch then waits on its slowest member, which is wasteful.
The right idea is a worker pool:
- Set up one shared cursor,
nextIndex = 0. - Start
limitworkers, each one an async function. - Each worker loops: grab the task the cursor points at (cursor +1) → run it → write the outcome back into
results[i]→ back to the top of the loop to grab the next one. - Queue empty, the worker exits on its own.
await Promise.all(workers)waits for every worker to clock out.
Only limit workers are ever running at once, so the concurrency count cannot go over by construction. Results are written back at index results[i], so the order is right automatically and no extra sorting is needed.
Measured output of this approach in the project (npm run q2): running now never reaches 3, the final 6 results come out in input order, and task 3 shows up as rejected. The full implementation is in the Q2 lesson of the React course.
try/catch 包住 awaitWrapping await in try/catch
await 遇到 rejected 会抛异常, 行为和 throw 一样。所以要接住它,就用普通的try/catch:
Federation 那道题的每个 resolver 都要求这个结构 —— TODO 原文写的是「with proper error handling」。
When await meets a rejected Promise it throws, behaving exactly like throw. So to catch it, you use an ordinary try/catch:
Every resolver in the Federation question wants this shape — the TODO reads “with proper error handling”.
react-notes-app/q2/taskRunner.ts动手做Get your hands on it
填空只是过渡。真正掌握的标准,是在没有答案的时候从头写出来 —— 所以做完 L2 之后一定要往 L3、L4 走。Filling blanks is a stepping stone. The real bar is writing it from nothing, so once L2 is comfortable, push on to L3 and L4.
Q2 的要求原文:「The runner NEVER throws, even if some tasks reject. It resolves with an array of results IN THE SAME ORDER as tasks.」 这描述的是哪个内置方法的语义?
The exact wording of the Q2 requirement: “The runner NEVER throws, even if some tasks reject. It resolves with an array of results IN THE SAME ORDER as tasks.” Which built-in method behaves like that?
Task<T> = () => Promise<T>。 如果题目改成传一个 Promise<T>[] 进来, 会出什么问题?
Task<T> = () => Promise<T>. If the question passed in a Promise<T>[] instead, what would go wrong?
这是 subgraph 里真实的 DataLoader 批量加载函数。 两个空都关系到「异步 + 数组」的固定套路。
This is the real DataLoader batch function from the subgraph. Both blanks are part of the fixed pattern for async work over an array.
换一道题也能用Works on other problems too
考试不会原题重考。真正能带走的是「看到这种信号 → 伸手去拿这个解法」。The exam will not reuse the same question. What you take away is the reflex: see this signal, reach for that solution.
- Promise 三态,只定一次;await 成功给值、失败抛异常。A Promise has three states and settles only once. await gives you the value on success and throws on failure.
- () => Promise<T> 是函数,Promise<T> 是已经在跑的事 —— 并发控制只能靠前者。() => Promise<T> is a function. Promise<T> is work that has already started. Only the first form lets you control how many run at once.
- all 一个失败就整体失败;allSettled 全等完再汇总。两者都保证顺序。all fails as a whole as soon as one fails. allSettled waits for all of them and then reports. Both keep the order.
- 并发上限 = 开 limit 个 worker 抢同一个游标,结果按下标写回自动保序。To cap how many run at once, start limit workers that share one cursor, and write each result back at its own index so the order is kept.
- await 要用 try/catch 接;catch 之后循环继续,才叫「不抛错」。Catch an await with try/catch. Only if the loop carries on after the catch does the function really not throw.