DrillLab
练习Practice

动手做Get your hands on it

练习跟着课文走 —— 每节课尾都有本课的练习。这一页是全部练习的总库,想集中刷题的时候来。 每个练习都写清了它来自哪一节,卡住了就回去看那一节。Practice follows the lessons — every lesson ends with the exercises for that lesson. This page is the whole library, for when you want to drill in one sitting. Each exercise names the lesson it came from, so you can go back when you stall.

0 / 148个做对过you got right

练习Exercises

筛出 148 个练习 · 第 5 / 13 页。Showing 148 · page 5 / 13.
来自From 实现:worker pool(工人池)Building it: a worker pool, meaning a fixed number of workers sharing one queue · React 考试React exam
L3写整块Write a block从签名开始,自己写出整个 runTasksStart from the signature and write all of runTasks yourself

只给签名。这是 Q2 的完整答案,写对了这道题就通了。 写完可以在本机 npm run q2 验证。

You get only the signature. This is the complete answer to Q2: get it right and the question is done. When you finish you can check it here with npm run q2.

要求Requirements
  • 同一时刻最多 limit 个任务在运行At most limit tasks are running at the same time
  • 某个任务结束后,立刻启动下一个(不是等一批都结束)As soon as one task finishes, start the next one (do not wait for a whole batch)
  • 任何任务失败都不能让整体抛错No failing task may make the whole call throw
  • 返回数组的顺序必须与 tasks 一致The order of the returned array must match tasks
  • 成功写 { status: "fulfilled", value },失败写 { status: "rejected", reason }On success write { status: "fulfilled", value }; on failure write { status: "rejected", reason }
TypeScriptq2/taskRunner.ts
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 实现:worker pool(工人池)Building it: a worker pool, meaning a fixed number of workers sharing one queue · React 考试React exam
L2Debug LabDebug LabDebug Lab · 一行 START 都没打印Debug Lab · not one START line prints

npm run q2,没有报错,但一行task N START 都没有,直接就出结果了 —— 而且结果里的 value 长得很奇怪。

You run npm run q2. Nothing reports an error, but not one task N START line appears; the results come out right away. And the value in each result looks strange.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npm run q2 === FINAL RESULTS (must be in original order) === #1 { status: 'fulfilled', value: [Function (anonymous)] } #2 { status: 'fulfilled', value: [Function (anonymous)] } #3 { status: 'fulfilled', value: [Function (anonymous)] } #4 { status: 'fulfilled', value: [Function (anonymous)] } #5 { status: 'fulfilled', value: [Function (anonymous)] } #6 { status: 'fulfilled', value: [Function (anonymous)] } # 注意: # - 一行 "task N START" 都没有 # - 应该 reject 的 task 3 也变成了 fulfilled # - value 是函数,不是 "result of task N"$ npm run q2 === FINAL RESULTS (must be in original order) === #1 { status: 'fulfilled', value: [Function (anonymous)] } #2 { status: 'fulfilled', value: [Function (anonymous)] } #3 { status: 'fulfilled', value: [Function (anonymous)] } #4 { status: 'fulfilled', value: [Function (anonymous)] } #5 { status: 'fulfilled', value: [Function (anonymous)] } #6 { status: 'fulfilled', value: [Function (anonymous)] } # Note: # - not one "task N START" line was printed # - task 3, which should reject, came back fulfilled # - value is a function, not "result of task N"
TypeScript有问题的 workerThe worker with the problem示意Illustrative
1const worker = async () => {
2 while (nextIndex < tasks.length) {
3 const i = nextIndex;
4 nextIndex++;
5
6 try {
7 const value = await tasks[i];
8 results[i] = { status: "fulfilled", value };
9 } catch (reason) {
10 results[i] = { status: "rejected", reason };
11 }
12 }
13};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式一 · Todo ListVariation 1 · Todo List · React 考试React exam
L2填空Fill the blanks补全翻转与批量操作Fill in the toggle and the bulk actionDrillLab 自出Written by DrillLab

四个空。第 2 个是「只改一个字段」的写法,第 4 个考的是 「全选」和「反选」的区别。

Four blanks. The second is how you change one field only. The fourth is about the difference between select-all and invert-selection.

TSXsrc/components/TodoList/index.tsx4 个空4 blanks
1// 翻转一条:只改 done,其他字段照抄
2const toggle = (id: number) => {
3 setTodos((prev) =>
4 prev.((t) => (t.id === id ? { , done: !t.done } : t)),
5 );
6};
7
8// 剩余几项 —— 派生数据
9const remaining = todos.((t) => !t.done).length;
10
11// 全选 / 取消全选
12const toggleAll = () => {
13 const next = ;
14 setTodos((prev) => prev.map((t) => ({ ...t, done: next })));
15};
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 变式一 · Todo ListVariation 1 · Todo List · React 考试React exam
L3写整块Write a block自己写出筛选与「清除已完成」Write the filtering and the clear-completed action yourselfDrillLab 自出Written by DrillLab

已有 todosfilter 两个 state。 写出可见列表和「清除已完成」,注意筛选态下的写操作该作用于谁。

You already have the two states todos and filter. Write the visible list and the clear-completed action, and think about which list a write should act on while a filter is on.

要求Requirements
  • visible 是派生数据,不许用 useState 或 useEffectvisible is derived data: no useState and no useEffect
  • filter 为 "all" 时显示全部,"active" 显示未完成,"done" 显示已完成When filter is "all" show everything, "active" shows the unfinished ones, "done" shows the finished ones
  • clearDone 移除所有已完成项,保留未完成项clearDone removes every finished item and keeps the unfinished ones
  • 写操作必须作用于 todos,不能作用于 visibleA write has to act on todos, never on visible
  • 不许修改原数组Do not change the original array
TSXsrc/components/TodoList/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式二 · 计时器:useEffect 的清理函数Variation 2 · a timer: the useEffect cleanup function · React 考试React exam
L2填空Fill the blanks补全计时器的 effectFill in the effect of the timerDrillLab 自出Written by DrillLab

三个空,全在这九行里。第 2 个空漏了会「越跳越快」, 第 3 个空写错会「卡在 1 不动」。

Three blanks, all within these nine lines. Miss the second and the clock speeds up with every start. Get the third wrong and the display freezes at 1.

TSXsrc/components/Timer/index.tsx3 个空3 blanks
1useEffect(() => {
2 if (!running) return;
3
4 const id = setInterval(() => {
5 setSeconds();
6 }, 1000);
7
8 return () => (id);
9}, []);
把 3 个空都填上才能检查(还差 3 个)Fill all 3 blanks to check (3 to go)
来自From 变式二 · 计时器:useEffect 的清理函数Variation 2 · a timer: the useEffect cleanup function · React 考试React exam
L3写整块Write a block自己写出整个计时器Write the whole timer yourselfDrillLab 自出Written by DrillLab

两个 state、一个 effect、一个 reset、一个 mm:ss 格式化。 检查器会专门查清理函数和函数式更新。

Two states, one effect, one reset, and one mm:ss formatter. The checker looks specifically for the cleanup function and the updater form.

要求Requirements
  • format(65) 要返回 "01:05",个位数补零format(65) has to return "01:05", padding single digits with a zero
  • 点 Start 开始每秒加一,点 Pause 停下并保留当前值Start begins adding one per second; Pause stops and keeps the current value
  • Reset 停下来并清零(按钮文字回到 Start)Reset stops and goes back to zero (the button text returns to Start)
  • effect 必须返回清理函数清掉定时器The effect has to return a cleanup function that clears the interval
  • 必须用函数式更新,避免过期闭包Use the updater form, so there is no stale closure
  • 按钮文字:跑着显示 Pause,停着显示 StartButton text: Pause while running, Start while stopped
TSXsrc/components/Timer/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式二 · 计时器:useEffect 的清理函数Variation 2 · a timer: the useEffect cleanup function · React 考试React exam
L2Debug LabDebug LabDebug Lab · 计时器越跑越快Debug Lab · the timer keeps getting fasterDrillLab 自出Written by DrillLab

点了几次 Start / Pause 之后,秒数开始一次跳好几秒。 下面是真实的测试输出。

After a few clicks of Start and Pause, the seconds start jumping several at a time. Below is the real test output.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ npx vitest run src/Timer.test.tsx ✕ pause stops the clock and keeps the value Expected element to have text content: 00:02 Received: 00:07 ✕ start/pause many times does not speed up Expected element to have text content: 00:04 Received: 00:10 ✕ reset stops and zeroes Expected element to have text content: 00:00 Received: 00:03 ✕ unmount clears the interval AssertionError: expected 1 to be +0 // Object.is equality Tests 4 failed | 4 passed (8) # 现象:start / pause 来回点四次,每次只走 1 秒, # 显示却是 00:10 —— 正好是 1+2+3+4。 # 而且 Reset 之后秒数还在自己往上涨。$ npx vitest run src/Timer.test.tsx ✕ pause stops the clock and keeps the value Expected element to have text content: 00:02 Received: 00:07 ✕ start/pause many times does not speed up Expected element to have text content: 00:04 Received: 00:10 ✕ reset stops and zeroes Expected element to have text content: 00:00 Received: 00:03 ✕ unmount clears the interval AssertionError: expected 1 to be +0 // Object.is equality Tests 4 failed | 4 passed (8) # Symptom: click start / pause four times, one second of running each time, # and the display reads 00:10 — exactly 1+2+3+4. # After Reset the seconds also keep climbing on their own.
TSXsrc/components/Timer/index.tsx示意Illustrative
1useEffect(() => {
2 if (!running) return;
3
4 const id = setInterval(() => {
5 setSeconds((s) => s + 1);
6 }, 1000);
7}, [running]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L2填空Fill the blanks补全取数 effect 的四个关键位置Fill in the four key spots of the fetching effectDrillLab 自出Written by DrillLab

四个空。第 1 和第 4 个合起来解决竞态,第 2 个是 fetch 的经典坑。

Four blanks. The first and the fourth together settle the race. The second is the classic fetch trap.

TSXsrc/components/UserCard/index.tsx4 个空4 blanks
1useEffect(() => {
2 let = false;
3
4 setLoading(true);
5 setError(null);
6 setUser(null);
7
8 (async () => {
9 try {
10 const res = await fetch(`/api/users/${userId}`);
11 if (!res.) throw new Error(`HTTP ${res.status}`);
12 const data: User = await res.json();
13 if (!ignore) setUser(data);
14 } catch (e) {
15 if (!ignore) setError((e as Error).message);
16 } finally {
17 if (!ignore) setLoading(false);
18 }
19 })();
20
21 return () => { ignore = ; };
22}, []);
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L3写整块Write a block自己写出带竞态防护的取数 effectWrite the fetching effect with race protection yourselfDrillLab 自出Written by DrillLab

三个 state 已给好。写出 effect 和三个提前返回。 检查器会查 res.ok、清理函数、竞态防护和 AbortError 过滤。

The three states are given. Write the effect and the three early returns. The checker looks for res.ok, the cleanup function, the race protection and the AbortError filter.

要求Requirements
  • 从 /api/users/{userId} 取数Fetch from /api/users/{userId}
  • 非 2xx 响应要当成错误处理,错误信息形如 HTTP 404Treat any non-2xx response as an error, with a message like HTTP 404
  • userId 变化时重新取数,并把上一次的结果作废(竞态防护)Refetch when userId changes, and void the previous result (race protection)
  • 用 AbortController 掐掉在途请求,但 AbortError 不展示给用户Use AbortController to cut off the request in flight, but never show AbortError to the user
  • 渲染顺序:loading → error → 空数据 → 正常数据Render order: loading, then error, then no data, then the data
  • effect 本身不能是 async 函数The effect itself must not be an async function
TSXsrc/components/UserCard/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.

来自From 变式三 · fetch 取数:loading、error 与竞态Variation 3 · fetching data: loading, error, and the race between two requests · React 考试React exam
L3Debug LabDebug LabDebug Lab · URL 上是用户 2,界面显示用户 1Debug Lab · the URL says user 2 and the screen shows user 1DrillLab 自出Written by DrillLab

快速点两个用户,界面最后显示的是先点的那个。 慢一点点就没问题。控制台干净。

Click two users quickly and the screen ends up showing the one you clicked first. Click a little slower and it is fine. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ npx vitest run src/UserCard.test.tsx ✕ a slow stale response must not overwrite the newer one(竞态) Expected element to have text content: 用户2 Received: 用户1 Tests 1 failed | 5 passed (6) # 手动复现: # 1. 点用户 1(这个接口慢,200ms) # 2. 立刻点用户 2(这个快,10ms) # 3. 先看到用户 2 —— 对的 # 4. 200ms 后界面自己变成了用户 1 ← 错的,URL 上还是 2# No error at all. $ npx vitest run src/UserCard.test.tsx ✕ a slow stale response must not overwrite the newer one(竞态) Expected element to have text content: 用户2 Received: 用户1 Tests 1 failed | 5 passed (6) # Manual repro: # 1. Click user 1 (that request is slow, 200ms) # 2. Click user 2 right away (that one is fast, 10ms) # 3. User 2 shows up first — correct # 4. 200ms later the view switches itself to user 1 ← wrong, the URL still says 2
TSXsrc/components/UserCard/index.tsx示意Illustrative
1useEffect(() => {
2 setLoading(true);
3 setError(null);
4
5 (async () => {
6 try {
7 const res = await fetch(`/api/users/${userId}`);
8 if (!res.ok) throw new Error(`HTTP ${res.status}`);
9 setUser(await res.json());
10 } catch (e) {
11 setError((e as Error).message);
12 } finally {
13 setLoading(false);
14 }
15 })();
16}, [userId]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L2填空Fill the blanks补全递归统计与递归渲染Fill in the recursive count and the recursive renderDrillLab 自出Written by DrillLab

四个空。第 2 个是递归调用本身,第 4 个是「往下一层」。

Four blanks. The second is the recursive call itself, and the fourth is one level further down.

TSXsrc/components/CommentTree/index.tsx4 个空4 blanks
1// 递归统计总条数
2export function countComments(nodes: Comment[]): number {
3 return nodes.reduce((sum, n) => sum + + (n.replies), 0);
4}
5
6// 递归渲染
7{comment.replies.length > 0 && (
8 <ul>
9 {comment.replies.map((child) => (
10 <
11 key={child.id}
12 comment={child}
13 depth={}
14 onReply={onReply}
15 />
16 ))}
17 </ul>
18)}
把 4 个空都填上才能检查(还差 4 个)Fill all 4 blanks to check (4 to go)
来自From 变式四 · 递归读取评论的评论Variation 4 · reading replies to replies with recursion · React 考试React exam
L3写整块Write a block写出树形数据的不可变更新Write an immutable update for tree dataDrillLab 自出Written by DrillLab

这是这道题真正的难点。目标节点可能在任意深度, 要返回一棵新树,而且原树一个字节都不能改

This is the hard part of the question. The target node can be at any depth, you have to return a new tree, and not one byte of the original may change.

要求Requirements
  • 找到 id === parentId 的节点,把 reply 追加到它的 replies 末尾Find the node whose id === parentId and append reply to the end of its replies
  • 返回新数组、新节点对象,不修改原数据Return a new array and new node objects, without changing the original data
  • 目标可能在任意深度,需要递归往下找The target can be at any depth, so recurse downwards to find it
  • 不许用 JSON.parse(JSON.stringify(...)) 深拷贝Do not deep-copy with JSON.parse(JSON.stringify(...))
  • 不许用 push / splice / 直接赋值Do not use push / splice / direct assignment
TypeScriptsrc/components/CommentTree/index.tsx
This check is textual: it looks for the right constructs, it does not run your code
提示Hints共 4 级,已看 0 级4 levels, 0 opened
先自己想两分钟。想不出来再点右上角 —— 提示是一级一级放的,不会一次给完。Think for two minutes first. Then use the button above — hints come one level at a time, never all at once.

看答案之前,先确认你已经自己动手写过一遍。看懂别人的答案和自己写出来,是两种能力。Before you open this, make sure you have written it yourself once. Following someone else's answer and producing your own are two different skills.