读题:三条要求,每一条都在指定一种写法Reading the question: three requirements, and each one decides how you write it
题面就写在 taskRunner.ts 的文件头注释里。逐条翻译。The question is written in the header comment of taskRunner.ts. Take it one requirement at a time.
这一页有什么On this page7
- 01 题面原文The question, word for word
- 02 三条要求逐条翻译The three requirements, one at a time
- 03 为什么是函数数组:这是整道题的支点Why it is an array of functions: this is what the whole question turns on
- 04 SettledResult:一个可辨识联合SettledResult: a discriminated union
- 05 验证台 demo.ts 怎么读How to read demo.ts, the check harness
- 练习 · 动手做Practice
- 迁移模式Transfer
- 复述三条要求,并说清每条排除了哪种实现Restate the three requirements, and say which implementation each one rules out
- 解释为什么参数是「函数数组」而不是「Promise 数组」Explain why the parameter is an array of functions and not an array of Promise values
- 看懂 SettledResult 这个可辨识联合类型Read the SettledResult type and see that it is a discriminated union
- 知道怎么跑 demo.ts 以及怎么读它的输出Know how to run demo.ts and how to read its output
这道题没有断言测试,只有一个打印实时并发数的 demo.ts。也就是说:验收全靠你自己会不会读那段输出。读不懂输出,就不知道自己做对没有。This question has no assertion tests. It has one demo.ts that prints how many tasks are running at each moment. So the only check is whether you can read that output. If you cannot read it, you do not know whether your answer is right.
react-notes-app/q2/taskRunner.ts题面 + 类型 + 要实现的函数The question, the types, and the function you must write
提醒:源项目在磁盘上是做完的版本 —— 下面就是答案。想自己先写一遍的话,现在关上。Heads up: on disk this project is the finished version — what follows is the answer. Close this if you want to write it yourself first.
react-notes-app/q2/taskRunner.tsreact-notes-app/q2/demo.ts验证台,打印实时并发数与最终结果The check harness: it prints how many tasks run at each moment, then the final results
react-notes-app/q2/demo.ts题面原文The question, word for word
注意它是英文的,而且每一条都很精确。Note that it is written in English, and every line is precise.
这段注释就在 q2/taskRunner.ts 的最上面。 下面的 // TODO: implement me 就是你要填的地方 (磁盘上的项目里这一句注释还留着,但下面已经有完整实现了):
This comment sits right at the top of q2/taskRunner.ts. The // TODO: implement me underneath is the spot you fill in (in the project on disk that comment is still there, but a complete implementation already sits below it):
react-notes-app/q2/taskRunner.ts三条要求逐条翻译The three requirements, one at a time
| 原文 | 中文 | 它排除了什么写法 |
|---|---|---|
tasks is an array of FUNCTIONS | 传进来的是一堆还没被调用的函数, 调用它才会开始干活 | 排除了「直接 await 数组元素」—— 必须先 tasks[i]() 调用 |
At most limit tasks may be RUNNING at the same time | 同一时刻最多 limit 个在跑。 必须等其中一个结束,才能开下一个 | 排除了 Promise.allSettled(tasks.map(t => t()))—— 那会一次全开 |
| NEVER throws … results IN THE SAME ORDER | 任何任务失败都不能让整体抛错; 结果数组的顺序必须和输入一致 | 排除了 Promise.all(一个失败就整体炸); 也排除了「谁先完成谁先 push」(顺序会乱) |
注释最后一句直接给了答案的形状:This mimics Promise.allSettled, but with a concurrency throttle.—— 「allSettled 的语义 + 一个并发节流」。 题面已经把要做什么说清楚了,剩下的是怎么做。
| Original | In plain words | What it rules out |
|---|---|---|
tasks is an array of FUNCTIONS | What arrives is a pile of functions that have not been called yet; calling one is what starts the work | Rules out a plain await on the array elements — you have to call tasks[i]() first |
At most limit tasks may be RUNNING at the same time | At most limit of them run at any moment. One has to finish before the next can start | Rules out Promise.allSettled(tasks.map(t => t()))— that opens all of them at once |
| NEVER throws … results IN THE SAME ORDER | No failing task may make the whole thing throw; the result array must be in the same order as the input | Rules out Promise.all (one failure fails the whole batch), and also “push whoever finishes first” (the order scrambles) |
The last line of the comment hands you the shape of the answer: This mimics Promise.allSettled, but with a concurrency throttle.— “allSettled semantics plus one concurrency throttle”. The brief already says what to build; the rest is how.
为什么是函数数组:这是整道题的支点Why it is an array of functions: this is what the whole question turns on
如果传进来的是 Promise,这道题根本无解。If you were handed Promise values, this question would have no answer at all.
Promise 一旦被创建,就已经在跑了,没有暂停键。
所以如果签名是 runTasks(promises: Promise<T>[], limit), 那么调用方写 runTasks([fetch(a), fetch(b), fetch(c)], 2)的那一瞬间,三个请求就已经同时发出去了。 你在函数内部再怎么排队都没意义 —— 网络请求早出去了。
改成 () => Promise<T> 之后, 调用方交给你的是「怎么开始」的说明书, 而不是「已经开始的事」。什么时候撕开说明书、 撕几张,完全由你决定。这才有并发控制的余地。
这个设计在真实世界里到处都是:批量上传文件时限制同时上传数、 爬虫限制并发请求、数据库连接池。看到「限制同时进行的数量」, 第一反应就该是「参数得是工厂函数,不能是已启动的任务」。
Once a Promise exists it is already running. There is no pause button.
So if the signature were runTasks(promises: Promise<T>[], limit), then the instant the caller writes runTasks([fetch(a), fetch(b), fetch(c)], 2) all three requests are already out the door at the same time. Queueing inside your function means nothing — the network calls left long ago.
Switch to () => Promise<T> and what the caller hands you is instructions for how to start, not a thing that already started. When you tear an instruction sheet off, and how many you tear off, is entirely your call. That is what leaves room for concurrency control.
This design is everywhere in the real world: capping simultaneous uploads in a batch, throttling a crawler’s parallel requests, a database connection pool. When you see “limit how many run at once”, the first thought should be “the parameter has to be factory functions, not tasks that already started”.
react-notes-app/q2/demo.tsSettledResult:一个可辨识联合SettledResult: a discriminated union
SettledResult<T> 是两个对象形状的联合: 要么有 value,要么有 reason, 两者不会同时存在。
status 这个字段是判别标签: 它的值是字面量 "fulfilled" 或"rejected"。 写了 if (r.status === "fulfilled") 之后, TypeScript 就知道这个分支里一定有 value且一定没有 reason。这叫类型收窄,这种类型叫可辨识联合(discriminated union)。
这也是为什么这里必须用 type 而不能用interface —— interface 不能表达「A 或 B」。
实用含义:你写结果时必须严格按这两种形状之一来。 写成 { status: "fulfilled", value, reason: undefined }会类型报错。
SettledResult<T> is a union of two object shapes: either there is a value or there is a reason, and the two never coexist.
The status field is the discriminant tag: its value is the literal "fulfilled" or "rejected". Once you write if (r.status === "fulfilled"), TypeScript knows this branch definitely has value and definitely has no reason. That is called narrowing, and this kind of type is a discriminated union.
It is also why this has to be a type and cannot be an interface — an interface cannot express “A or B”.
Practical consequence: when you write a result it must match one of those two shapes exactly. Writing { status: "fulfilled", value, reason: undefined }is a type error.
验证台 demo.ts 怎么读How to read demo.ts, the check harness
这道题没有断言测试。会读输出,等于会判卷。This question has no assertion tests. Reading the output is the grading.
demo.ts 用一个模块级变量 running记录「此刻有几个任务在跑」:任务开始时 running++, 结束时 running--,并且每次都打印出来。
它准备了 6 个任务,其中第 3 个会 reject, 然后用 limit = 2 调用。
所以验收标准是三条,全靠肉眼:
running now永远不超过 2。出现 3 就是并发控制失效。- 最终 6 条结果的顺序与输入一致。
#1必须是 task 1 的结果,即使它跑得最慢。 - task 3 以
rejected出现, 而且 4、5、6 照样跑完了。如果程序在 task 3 之后就崩了,说明没接住错误。
demo.ts keeps one module-level variable running to track how many tasks are in flight right now: running++ when a task starts, running-- when it ends, and it prints the number every time.
It sets up 6 tasks, the third of which rejects, then calls with limit = 2.
So there are three acceptance criteria, all judged with your eyes:
running nownever goes above 2. A 3 means the throttle is broken.- The final 6 results come back in the input order.
#1has to be task 1’s result even though it is the slowest. - Task 3 shows up as
rejected, and 4, 5, 6 still run to completion. If the program dies after task 3, the error was never caught.
react-notes-app/q2/demo.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.
假设签名改成 runTasks(promises: Promise<T>[], limit: number), 调用方写 runTasks([task1(), task2(), task3()], 2)。 会发生什么?
Suppose the signature becomes runTasks(promises: Promise<T>[], limit: number) and the caller writes runTasks([task1(), task2(), task3()], 2). What happens?
有人写 return Promise.allSettled(tasks.map((t) => t()))。 它满足几条要求?
Someone writes return Promise.allSettled(tasks.map((t) => t())). How many of the requirements does it meet?
换一道题也能用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.
- 三条要求:函数数组、并发上限 limit、绝不抛错且保序。Three requirements: an array of functions, a concurrency limit called limit, and never throwing while keeping the order.
- 参数是 () => Promise<T> 而不是 Promise<T>,因为 Promise 一创建就没法暂停。The parameter is () => Promise<T> and not Promise<T>, because once a Promise exists you cannot pause it.
- Promise.allSettled(tasks.map(t => t())) 满足两条但违反并发上限 —— 难点全在节流。Promise.allSettled(tasks.map(t => t())) meets two requirements but breaks the concurrency limit. All the difficulty is in the throttling.
- SettledResult 是可辨识联合,靠 status 字段收窄类型,必须用 type 不能用 interface。SettledResult is a discriminated union: the status field narrows the type, and it must be declared with type, not interface.
- 这道题没有断言测试,验收靠读 demo.ts 的三条输出特征。This question has no assertion tests. You check it by reading three things in the demo.ts output.