DrillLab
第 13 / 21 节LESSON 13 / 21约 12 分钟~12 min

读题:三条要求,每一条都在指定一种写法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.

2 个练习2 exercisesReact · 第 4 部分React · Part 4
这一页有什么On this page7
学完这节你会After this lesson you can
  • 复述三条要求,并说清每条排除了哪种实现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
这在考试里考什么What the exam does with this

这道题没有断言测试,只有一个打印实时并发数的 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.

这节课要看的真实文件Real files this lesson looks at高亮行 = 需要你动手改的文件Highlighted rows are the files you edit
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.

TypeScripttaskRunner.ts源项目From source
1// Q2: Implement a custom asynchronous task runner.
2//
3// Requirements:
4// 1. `tasks` is an array of FUNCTIONS. Each function, when called,
5// starts an async job and returns a Promise.
6// 2. At most `limit` tasks may be RUNNING at the same time.
7// A new task may only start after one of the running tasks finishes.
8// 3. The runner NEVER throws, even if some tasks reject.
9// It resolves with an array of results IN THE SAME ORDER as `tasks`:
10// { status: "fulfilled", value: T } for tasks that succeeded
11// { status: "rejected", reason: unknown } for tasks that failed
12// (This mimics Promise.allSettled, but with a concurrency throttle.)
13
14export type Task<T> = () => Promise<T>;
15
16export type SettledResult<T> =
17 | { status: "fulfilled"; value: T }
18 | { status: "rejected"; reason: unknown };
19
20export async function runTasks<T>(
21 tasks: Task<T>[],
22 limit: number,
23): Promise<SettledResult<T>[]> {
24 // TODO: implement me
25 const results: SettledResult<T>[] = new Array(tasks.length);
26
27 let nextIndex = 0;
28
29 const worker = async () => {
30 while (nextIndex < tasks.length) {
31 const i = nextIndex;
32 nextIndex++;
33
34 try {
35 const value = await tasks[i]();
36 results[i] = { status: "fulfilled", value };
37 } catch (reason) {
38 results[i] = { status: "rejected", reason };
39 }
40 }
41 };
42 if (tasks.length === 0) return [];
43
44 const workerCount = Math.min(limit, tasks.length);
45 const workers: Promise<void>[] = [];
46 for (let w = 0; w < workerCount; w++) {
47 workers.push(worker());
48 }
49 await Promise.all(workers);
50 return results;
51}
Source: react-notes-app/q2/taskRunner.ts
react-notes-app/q2/demo.ts验证台,打印实时并发数与最终结果The check harness: it prints how many tasks run at each moment, then the final results
TypeScriptdemo.ts源项目From source
1// Test harness for Q2. Run with: npm run q2
2// It prints how many tasks are running at each moment, so you can
3// VERIFY the concurrency never exceeds the limit.
4
5import { runTasks, type Task } from "./taskRunner";
6
7let running = 0;
8
9const makeTask = (id: number, ms: number, shouldFail = false): Task<string> => {
10 return () =>
11 new Promise((resolve, reject) => {
12 running++;
13 console.log(`task ${id} START (running now: ${running})`);
14 setTimeout(() => {
15 running--;
16 if (shouldFail) {
17 console.log(`task ${id} FAIL (running now: ${running})`);
18 reject(new Error(`task ${id} failed`));
19 } else {
20 console.log(`task ${id} DONE (running now: ${running})`);
21 resolve(`result of task ${id}`);
22 }
23 }, ms);
24 });
25};
26
27const tasks = [
28 makeTask(1, 300),
29 makeTask(2, 100),
30 makeTask(3, 200, true), // this one rejects
31 makeTask(4, 100),
32 makeTask(5, 150),
33 makeTask(6, 100),
34];
35
36runTasks(tasks, 2).then((results) => {
37 console.log("\n=== FINAL RESULTS (must be in original order) ===");
38 results.forEach((r, i) => console.log(`#${i + 1}`, r));
39});
Source: react-notes-app/q2/demo.ts
§01

题面原文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):

TypeScriptq2/taskRunner.ts(题面与签名)q2/taskRunner.ts (the question and the signature)源项目From source
1// Q2: Implement a custom asynchronous task runner.
2//
3// Requirements:
4// 1. `tasks` is an array of FUNCTIONS. Each function, when called,
5// starts an async job and returns a Promise.
6// 2. At most `limit` tasks may be RUNNING at the same time.
7// A new task may only start after one of the running tasks finishes.
8// 3. The runner NEVER throws, even if some tasks reject.
9// It resolves with an array of results IN THE SAME ORDER as `tasks`:
10// { status: "fulfilled", value: T } for tasks that succeeded
11// { status: "rejected", reason: unknown } for tasks that failed
12// (This mimics Promise.allSettled, but with a concurrency throttle.)
13
14export type Task<T> = () => Promise<T>;
15
16export type SettledResult<T> =
17 | { status: "fulfilled"; value: T }
18 | { status: "rejected"; reason: unknown };
19
20export async function runTasks<T>(
21 tasks: Task<T>[],
22 limit: number,
23): Promise<SettledResult<T>[]> {
24 // TODO: implement me
25}
Source: react-notes-app/q2/taskRunner.ts
§02

三条要求逐条翻译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 的语义 + 一个并发节流」。 题面已经把要做什么说清楚了,剩下的是怎么做。

OriginalIn plain wordsWhat it rules out
tasks is an array of FUNCTIONSWhat arrives is a pile of functions that have not been called yet; calling one is what starts the workRules 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 timeAt most limit of them run at any moment. One has to finish before the next can startRules out Promise.allSettled(tasks.map(t => t()))— that opens all of them at once
NEVER throws … results IN THE SAME ORDERNo failing task may make the whole thing throw; the result array must be in the same order as the inputRules 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.

§03

为什么是函数数组:这是整道题的支点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”.

TypeScriptq2/demo.ts 里 Task 是怎么造出来的How Task is built inside q2/demo.ts示意Illustrative
1// 调用方在 demo.ts 里是这么用的(真实代码)
2const makeTask = (id: number, ms: number, shouldFail = false): Task<string> => {
3 return () => // ← 注意这里返回的是一个函数
4 new Promise((resolve, reject) => {
5 running++;
6 console.log(`task ${id} START (running now: ${running})`);
7 ...
8 });
9};
10
11const tasks = [
12 makeTask(1, 300), // 只是造好了「说明书」,一个请求都还没发
13 makeTask(2, 100),
14 makeTask(3, 200, true),
15 ...
16];
1// This is how the caller uses it in demo.ts (real code)
2const makeTask = (id: number, ms: number, shouldFail = false): Task<string> => {
3 return () => // ← note that this returns a function
4 new Promise((resolve, reject) => {
5 running++;
6 console.log(`task ${id} START (running now: ${running})`);
7 ...
8 });
9};
10
11const tasks = [
12 makeTask(1, 300), // only builds the instructions; no request sent yet
13 makeTask(2, 100),
14 makeTask(3, 200, true),
15 ...
16];
Source: react-notes-app/q2/demo.ts
makeTask 返回的是「一个函数」,而不是「一个 Promise」。里面的 new Promise 只有在这个函数被调用时才执行 —— 这就是为什么 running++ 那一行在你调 tasks[i]() 之前不会跑。makeTask returns a function, not a Promise. The new Promise inside it runs only when that function is called. That is why the running++ line does not run until you call tasks[i]().
§04

SettledResult:一个可辨识联合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.

TypeScript可辨识联合怎么用How to use a discriminated union示意Illustrative
1const results = await runTasks(tasks, 2);
2
3for (const r of results) {
4 if (r.status === "fulfilled") {
5 console.log(r.value); // ✓ TypeScript 知道这里有 value
6 // console.log(r.reason); // ✗ 报错:这个分支里没有 reason
7 } else {
8 console.log(r.reason); // ✓ 这个分支里有 reason
9 }
10}
1const results = await runTasks(tasks, 2);
2
3for (const r of results) {
4 if (r.status === "fulfilled") {
5 console.log(r.value); // ✓ TypeScript knows value exists here
6 // console.log(r.reason); // ✗ error: this branch has no reason
7 } else {
8 console.log(r.reason); // ✓ this branch has reason
9 }
10}
§05

验证台 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 调用。

所以验收标准是三条,全靠肉眼:

  1. running now 永远不超过 2。出现 3 就是并发控制失效。
  2. 最终 6 条结果的顺序与输入一致。#1 必须是 task 1 的结果,即使它跑得最慢。
  3. 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:

  1. running now never goes above 2. A 3 means the throttle is broken.
  2. The final 6 results come back in the input order.#1 has to be task 1’s result even though it is the slowest.
  3. 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.
TypeScriptq2/demo.ts(全文)q2/demo.ts (the whole file)源项目From source
1let running = 0;
2
3const makeTask = (id: number, ms: number, shouldFail = false): Task<string> => {
4 return () =>
5 new Promise((resolve, reject) => {
6 running++;
7 console.log(`task ${id} START (running now: ${running})`);
8 setTimeout(() => {
9 running--;
10 if (shouldFail) {
11 console.log(`task ${id} FAIL (running now: ${running})`);
12 reject(new Error(`task ${id} failed`));
13 } else {
14 console.log(`task ${id} DONE (running now: ${running})`);
15 resolve(`result of task ${id}`);
16 }
17 }, ms);
18 });
19};
20
21const tasks = [
22 makeTask(1, 300),
23 makeTask(2, 100),
24 makeTask(3, 200, true), // this one rejects
25 makeTask(4, 100),
26 makeTask(5, 150),
27 makeTask(6, 100),
28];
29
30runTasks(tasks, 2).then((results) => {
31 console.log("\n=== FINAL RESULTS (must be in original order) ===");
32 results.forEach((r, i) => console.log(`#${i + 1}`, r));
33});
Source: react-notes-app/q2/demo.ts
Terminal已跑通Verified
1$ npm run q2 # → tsx q2/demo.ts
package.json 里的 q2 script 用 tsx 直接跑 TypeScript,不需要先编译。The q2 script in package.json uses tsx to run TypeScript directly, so there is no compile step first.
练习Practice

动手做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.

L1认出来Spot it如果参数改成 Promise 数组会怎样What happens if the parameter becomes an array of Promises

假设签名改成 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?

先选一个选项Pick an option first
L1认出来Spot it为什么不能直接用 Promise.allSettledWhy Promise.allSettled on its own is not the answer

有人写 return Promise.allSettled(tasks.map((t) => t()))。 它满足几条要求?

Someone writes return Promise.allSettled(tasks.map((t) => t())). How many of the requirements does it meet?

先选一个选项Pick an option first
迁移Transfer

换一道题也能用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.

「限制同时进行的数量」The wording: limit how many run at the same time
参数必须是工厂函数数组,不能是已启动的 PromiseThe parameter must be an array of functions that create the work, not Promise values that already started
「不管失败都要拿到全部结果」The wording: collect every result even when some fail
allSettled 的语义:try/catch 每一个,都记下来This is what allSettled means: wrap each one in try/catch and record what happened
「结果顺序与输入一致」The wording: results must be in the same order as the input
按下标写回预分配的数组,别用 pushWrite into an array you created up front, at the matching index; do not use push
看到 status: "a" | "b" 这种字段You see a field like status: "a" | "b"
可辨识联合,if 之后类型自动收窄A discriminated union: inside the if, the type narrows on its own
这节的要点What to take away
  1. 三条要求:函数数组、并发上限 limit、绝不抛错且保序。Three requirements: an array of functions, a concurrency limit called limit, and never throwing while keeping the order.
  2. 参数是 () => Promise<T> 而不是 Promise<T>,因为 Promise 一创建就没法暂停。The parameter is () => Promise<T> and not Promise<T>, because once a Promise exists you cannot pause it.
  3. 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.
  4. SettledResult 是可辨识联合,靠 status 字段收窄类型,必须用 type 不能用 interface。SettledResult is a discriminated union: the status field narrows the type, and it must be declared with type, not interface.
  5. 这道题没有断言测试,验收靠读 demo.ts 的三条输出特征。This question has no assertion tests. You check it by reading three things in the demo.ts output.

接下来What next

  1. 把这一节的练习做掉Do this lesson’s exercises2 个,就在这一页上面 —— 别攒着最后一起做2 of them, further up this page — do not save them for later
    回到练习 ↑Back up to them ↑
  2. 接着看下一节Continue to the next lesson实现:worker pool(工人池)Building it: a worker pool, meaning a fixed number of workers sharing one queue
    下一节Next lesson
读完并且做过上面的练习了吗?Read it and worked through the exercises above?
上一节:Previous: 四个测试逐条读,以及它们的盲区The four tests read line by line, and what they fail to catch