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

筛出 10 个练习(共 148 个)。Showing 10 of 148.
来自From useEffect:把 props 的变化同步进 stateuseEffect: copying a change in props into state · React 考试React exam
L3Debug LabDebug LabDebug Lab · 输入框敲不进字Debug Lab · the inputs will not take any typing

在 Title 输入框里打字,字符出现一瞬间又消失;点某一行的 Edit,表单能回填,但同样改不动。控制台没有任何报错。请判断类型并定位。

Type into the Title input and each character appears for an instant, then vanishes; press Edit on a row and the form fills in, but you cannot change it either. The console shows no error at all. Classify the error, then locate it.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 现象:在 Title 输入框里打字,字符出现一瞬间又消失;Add 按钮一直是灰的。 # npx vitest run:3 failed / 1 passed(adds a note / deletes a note / edits a note in place 失败)# No error at all. # Symptom: type into the Title input and each character appears for an instant, then vanishes; the Add button stays grey. # npx vitest run: 3 failed / 1 passed (adds a note / deletes a note / edits a note in place fail)
TSX有问题的依赖数组The broken dependency array示意Illustrative
1useEffect(() => {
2 if (noteToEdit) {
3 setTitle(noteToEdit.title);
4 setContent(noteToEdit.content);
5 } else {
6 setTitle("");
7 setContent("");
8 }
9}, [noteToEdit, title, content]);
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Task 3 · Edit:回填、改文字、就地更新、退出编辑Task 3 · Edit: refill the form, change the button text, update the row where it is, leave edit mode · React 考试React exam
L3Debug LabDebug LabDebug Lab · 点 Update 之后列表没有变化Debug Lab · the list does not change after you click Update

点 Edit,输入框正常回填,按钮变成 Update。改完内容点 Update —— 表单被清空、按钮回到 Add,看起来像提交成功了,可列表一点变化都没有。控制台干净。

Click Edit and the inputs prefill correctly, and the button becomes Update. Change the content and click Update — the form clears and the button goes back to Add, so it looks as if the update worked, but the list does not change at all. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 复现: # 1. 添加 "Old / c1" # 2. 点 Edit → 输入框显示 Old / c1,按钮变 Update ✓ 这两步正常 # 3. 把标题改成 "New",点 Update # 期望:列表里那条变成 New,表单清空,按钮回到 Add # 实际:表单清空了,按钮也回到了 Add,但列表还是 Old # 测试结果: # ✓ adds a note # ✓ submit button disabled when inputs empty # ✓ deletes a note # ✕ edits a note in place # expect(element).toHaveTextContent() # Expected element to have text content: New # Received: Oldc1EditDelete# No error at all. # Repro: # 1. Add "Old / c1" # 2. Click Edit → the inputs show Old / c1, the button reads Update ✓ both fine # 3. Change the title to "New" and click Update # Expected: that row becomes New, the form clears, the button goes back to Add # Actual: the form is cleared and the button is back to Add, but the row is still Old # Test results: # ✓ adds a note # ✓ submit button disabled when inputs empty # ✓ deletes a note # ✕ edits a note in place # expect(element).toHaveTextContent() # Expected element to have text content: New # Received: Oldc1EditDelete
TSX有问题的 handleSubmitNoteThe handleSubmitNote with the bug示意Illustrative
1const handleSubmitNote = (submittedNote: Note) => {
2 const note = { ...submittedNote, id: Date.now() };
3
4 if (noteToEdit) {
5 setNotes((prev) =>
6 prev.map((n) => (n.id === note.id ? note : n)),
7 );
8 setNoteToEdit(null);
9 } else {
10 setNotes((prev) => [...prev, note]);
11 }
12};
第 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
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 not to have text content: 用户1 ✕ aborts the in-flight request on unmount TypeError: Cannot read properties of undefined (reading 'aborted') Tests 2 failed | 4 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 (race condition) Expected element not to have text content: 用户1 ✕ aborts the in-flight request on unmount TypeError: Cannot read properties of undefined (reading 'aborted') Tests 2 failed | 4 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
L3Debug LabDebug LabDebug Lab · 回复加进去了,界面不动Debug Lab · the reply went in and the screen never movedDrillLab 自出Written by DrillLab

给深层评论加回复,console.log 打出来的树里新回复确实在,但界面没变化。控制台干净。

You add a reply to a deep comment. The tree printed by console.log really does contain the new reply, but the screen does not change. The console is clean.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ npx vitest run src/CommentTree.test.tsx ✕ addReply 挂到深层节点,且不改原树 TypeError: Cannot add property 0, object is not extensible (测试把原树深冻结了,实现试图直接修改它) ✕ 给三层的评论再回复,落在正确的位置 Unable to find an element with the text: 第四层 # 手动复现:点某条评论的 Reply、输入、发送 # console.log(comments) -> 新回复确实在树里 # 屏幕 -> 一点变化都没有# No error at all. $ npx vitest run src/CommentTree.test.tsx ✕ addReply attaches to a deep node and leaves the original tree untouched TypeError: Cannot add property 0, object is not extensible (The test deep-froze the original tree; the implementation edits it in place.) ✕ a reply to a third-level comment lands in the right place Unable to find an element with the text: 第四层 # Manual repro: click Reply on a comment, type something, send it # console.log(comments) -> the new reply really is in the tree # the screen -> nothing changes at all
TSXsrc/components/CommentTree/index.tsx示意Illustrative
1function addReply(nodes: Comment[], parentId: number, reply: Comment) {
2 for (const node of nodes) {
3 if (node.id === parentId) {
4 node.replies.push(reply); // 找到就塞进去
5 return nodes;
6 }
7 addReply(node.replies, parentId, reply);
8 }
9 return nodes;
10}
11
12const handleReply = (parentId: number, text: string) => {
13 const reply = { id: Date.now(), author: "我", body: text, replies: [] };
14 setComments(addReply(comments, parentId, reply));
15};
1function addReply(nodes: Comment[], parentId: number, reply: Comment) {
2 for (const node of nodes) {
3 if (node.id === parentId) {
4 node.replies.push(reply); // found it, so push it in
5 return nodes;
6 }
7 addReply(node.replies, parentId, reply);
8 }
9 return nodes;
10}
11
12const handleReply = (parentId: number, text: string) => {
13 const reply = { id: Date.now(), author: "我", body: text, replies: [] };
14 setComments(addReply(comments, parentId, reply));
15};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · React 十种典型故障Debug Lab · ten typical React failures · React 考试React exam
L3Debug LabDebug Lab故障 4 · 编辑后列表毫无变化(综合题)Fault 4 · the list does not change after an edit (mixed question)

这一题不告诉你是哪一类。控制台干净,console.log 显示数据是对的。自己分诊。

This one does not tell you which category it is. The console is clean, and console.log shows the data is correct. Sort it yourself.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 复现:添加 "A"、"B" 两条 → 点 B 的 Edit → 改成 "B2" → 点 Update # 期望:列表变成 A、B2 # 实际:列表还是 A、B,按钮还停在 Update # 在 handleSubmitNote 里插了日志: console.log("submitted:", submittedNote); // → submitted: { id: 1785737900978, title: 'B2', content: '...' } ← 数据是对的 console.log("after:", notes); // → after: [ {title:'A'...}, {title:'B2'...} ] ← 数组里也是对的! # 但屏幕上还是 B。 # 测试结果: # ✕ edits a note in place# No error at all. # Repro: add "A" and "B" → click Edit on B → change it to "B2" → click Update # Expected: the list becomes A, B2 # Actual: the list is still A, B, and the button is still on Update # Logs added inside handleSubmitNote: console.log("submitted:", submittedNote); // → submitted: { id: 1785737900978, title: 'B2', content: '...' } ← the data is right console.log("after:", notes); // → after: [ {title:'A'...}, {title:'B2'...} ] ← the array is right too! # But the screen still shows B. # Test result: # ✕ edits a note in place
TSX有问题的 handleSubmitNoteThe handleSubmitNote with the problem示意Illustrative
1const handleSubmitNote = (submittedNote: Note) => {
2 if (noteToEdit) {
3 const i = notes.findIndex((n) => n.id === submittedNote.id);
4 notes[i] = submittedNote;
5 setNotes(notes);
6 } else {
7 setNotes((prev) => [...prev, submittedNote]);
8 }
9};
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 三处埋雷:怎么系统地找出来The three planted bugs: how to find them systematically · Federation 考试Federation exam
L3Debug LabDebug LabDebug Lab · Cannot read properties of undefinedDebug Lab · Cannot read properties of undefined

Mutation.createOrder 的测试失败了。报错说在读一个 undefined 的属性。自己分诊。

The Mutation.createOrder test fails. The error says something read a property of undefined. Diagnose it yourself.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › Mutation.createOrder resolver › should create a new order successfully GraphQLError: Failed to create order 91 | } catch (error) { 92 | console.error(`[${correlationId}] Error creating order:`, error.message); > 93 | throw new GraphQLError('Failed to create order', { # 往上翻,console.error 打出的原始错误是: console.error [test-correlation-id] Error creating order: Cannot read properties of undefined (reading 'createOrder')● Order Resolvers › Mutation.createOrder resolver › should create a new order successfully GraphQLError: Failed to create order 91 | } catch (error) { 92 | console.error(`[${correlationId}] Error creating order:`, error.message); > 93 | throw new GraphQLError('Failed to create order', { # Scroll up: the raw error that console.error printed is console.error [test-correlation-id] Error creating order: Cannot read properties of undefined (reading 'createOrder')
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1const order = await dataSources.orderAPI.createOrder({ userId, items });
2
3// 参考:index.js 里 context 的 return
4// return {
5// dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
6// loaders: { shippingInfoLoader, orderLoader },
7// correlationId
8// };
1const order = await dataSources.orderAPI.createOrder({ userId, items });
2
3// For reference: what the context function in index.js returns
4// return {
5// dataSources: { orderDataSource, inventoryDataSource, shippingDataSource },
6// loaders: { shippingInfoLoader, orderLoader },
7// correlationId
8// };
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 三处埋雷:怎么系统地找出来The three planted bugs: how to find them systematically · Federation 考试Federation exam
L3Debug LabDebug LabDebug Lab · 错误码不对(不报错的那种 bug)Debug Lab · The wrong error code (the kind of bug that throws nothing)

代码跑得通,没有异常。但测试说错误码不对。这是三处埋雷里最值得理解的一处。

The code runs and raises no exception, but the test says the error code is wrong. Of the three planted bugs, this is the one most worth understanding.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
● Order Resolvers › Error handling › should return structured error for validation failures expect(received).toBe(expected) // Object.is equality Expected: "INVALID_INPUT" Received: "SERVICE_ERROR" # 测试代码: # const input = { userId: '789', items: [] }; ← 空 items,应该被校验拦下 # try { # await resolvers.Mutation.createOrder({}, input, context); # throw new Error('Should have thrown an error'); # } catch (error) { # expect(error.extensions.code).toBe('INVALID_INPUT'); # }● Order Resolvers › Error handling › should return structured error for validation failures expect(received).toBe(expected) // Object.is equality Expected: "INVALID_INPUT" Received: "SERVICE_ERROR" # The test code: # const input = { userId: '789', items: [] }; ← empty items, validation should stop it # try { # await resolvers.Mutation.createOrder({}, input, context); # throw new Error('Should have thrown an error'); # } catch (error) { # expect(error.extensions.code).toBe('INVALID_INPUT'); # }
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1try {
2 if (!userId || !items || items.length === 0) {
3 throw new GraphQLError('Invalid order input', {
4 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
5 });
6 }
7 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
8 return order;
9} catch (error) {
10 console.error(`[${correlationId}] Error creating order:`, error.message);
11 throw new GraphQLError('Failed to create order', {
12 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
13 });
14}
1try {
2 if (!userId || !items || items.length === 0) {
3 throw new GraphQLError('Invalid order input', {
4 extensions: { code: ErrorCodes.INVALID_INPUT, correlationId }
5 });
6 }
7 const order = await dataSources.orderDataSource.createOrder(userId, pricedItems);
8 return order;
9} catch (error) {
10 console.error(`[${correlationId}] Error creating order:`, error.message);
11 throw new GraphQLError('Failed to create order', {
12 extensions: { code: ErrorCodes.SERVICE_ERROR, correlationId }
13 });
14}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From 六个端点:状态码就是这道题的全部Six endpoints: the status codes are the whole task · Federation 考试Federation exam
L3Debug LabDebug LabDebug Lab · 查一个不存在的订单,返回了 200Debug Lab · Asking for an order that does not exist returns 200

五个测试全过。但手动 curl 一个不存在的 id,得到 200 和一个空 body。期望是 404 加一段 JSON。

All five tests pass. But curl an id that does not exist by hand and you get a 200 with an empty body. It should be a 404 with a piece of JSON.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
$ curl -i -s localhost:8080/api/orders/999 HTTP/1.1 200 Content-Length: 0 # 期望: # HTTP/1.1 404 # { "timestamp": "...", "status": 404, "message": "Order not found with id: 999" } # mvn test:Tests run: 5, Failures: 0 ← 测试全过!$ curl -i -s localhost:8080/api/orders/999 HTTP/1.1 200 Content-Length: 0 # Expected: # HTTP/1.1 404 # { "timestamp": "...", "status": 404, "message": "Order not found with id: 999" } # mvn test: Tests run: 5, Failures: 0 ← every test passes!
Java有问题的实现The broken implementation示意Illustrative
1@GetMapping("/api/orders/{id}")
2public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
3 logger.info("GET /api/orders/{} correlationId={}", id, correlationId());
4 try {
5 return ResponseEntity.ok(orderService.getOrderById(id));
6 } catch (EntityNotFoundException ex) {
7 return null;
8 }
9}
1@GetMapping("/api/orders/{id}")
2public ResponseEntity<Order> getOrderById(@PathVariable Long id) {
3 logger.info("GET /api/orders/{} correlationId={}", id, correlationId());
4 try {
5 return ResponseEntity.ok(orderService.getOrderById(id));
6 } catch (EntityNotFoundException ex) {
7 return null;
8 }
9}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures · Federation 考试Federation exam
L3Debug LabDebug Lab故障 1 · resolver 写了,字段还是 nullFault 1 · the resolver is written, the field is still null

你确信写了 shippingInfo 的实现,测试也不报错,但查询返回的 shippingInfo 是null。控制台里连你加的 console.error 探针都没打印。

You are sure you wrote an implementation for shippingInfo, and no test reports anything, but the query returns shippingInfo as null. Not even the console.error probe you added prints on the console.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 $ node verify-schema.mjs ... Query.orders + shippingInfo: {"orders":[ {"id":"order-456","status":"SHIPPED","totalAmount":299.99,"shippingInfo":null}, {"id":"order-457","status":"DELIVERED","totalAmount":89.99,"shippingInfo":null}]} | errors: [] ... # 你在 resolver 第一行加的 console.error('>>> shippingInfo called') # 一次都没打印。# No error at all. $ node verify-schema.mjs ... Query.orders + shippingInfo: {"orders":[ {"id":"order-456","status":"SHIPPED","totalAmount":299.99,"shippingInfo":null}, {"id":"order-457","status":"DELIVERED","totalAmount":89.99,"shippingInfo":null}]} | errors: [] ... # The console.error('>>> shippingInfo called') you added as the resolver's first line # never printed, not once.
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1export const resolvers = {
2 Order: {
3 async shipping(parent, _, { loaders }) { // ← 名字
4 console.error('>>> shippingInfo called');
5 return loaders.shippingInfoLoader.load(parent.id);
6 }
7 },
8 ...
9};
10
11// 参考 schema.graphql:
12// type Order {
13// ...
14// shippingInfo: ShippingInfo
15// }
1export const resolvers = {
2 Order: {
3 async shipping(parent, _, { loaders }) { // ← the name
4 console.error('>>> shippingInfo called');
5 return loaders.shippingInfoLoader.load(parent.id);
6 }
7 },
8 ...
9};
10
11// For reference, schema.graphql says:
12// type Order {
13// ...
14// shippingInfo: ShippingInfo
15// }
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this
来自From Debug Lab · Federation 十种典型故障Debug Lab · ten common Federation failures · Federation 考试Federation exam
L3Debug LabDebug Lab故障 3 · A 拿到了 B 的数据Fault 3 · A receives B's data

查两个订单的物流,返回的数据对上了错的订单。没有任何报错。这是 DataLoader 最隐蔽的一类误用。

You query the shipping info for two orders and the data comes back attached to the wrong order. Nothing reports an error. This is the hardest kind of DataLoader misuse to notice.

第 1 步 · 先看现象和代码,别急着改Step 1 · read the symptom and the code before you touch anything
# 没有任何报错。 # 查询:{ orders(userId:"123") { id shippingInfo { trackingNumber } } } # # 期望: # order-456 -> TRACK123456 # order-457 -> TRACK123457 # # 实际: # order-456 -> TRACK123457 ← 串了! # order-457 -> TRACK123456 ← 也串了 # 日志:[DataLoader] Batching 2 shipping info requests ← 合并是生效的 # (用 verify-schema.mjs 跑时,先注释掉它的第 30 行才看得到这行日志)# No error at all. # Query: { orders(userId:"123") { id shippingInfo { trackingNumber } } } # # Expected: # order-456 -> TRACK123456 # order-457 -> TRACK123457 # # Actual: # order-456 -> TRACK123457 ← got the other one's number! # order-457 -> TRACK123456 ← and so did this one # Log: [DataLoader] Batching 2 shipping info requests ← the batching does work # (with verify-schema.mjs you only see this line after commenting out its line 30)
JavaScriptsrc/resolvers/orderResolvers.js示意Illustrative
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const all = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 // 「按状态排个序,已送达的排前面」—— 看起来无害
10 return all.sort((a, b) => a.status.localeCompare(b.status));
11 });
12}
1function createShippingInfoLoader(shippingDataSource) {
2 return new DataLoader(async orderIds => {
3 console.log(`[DataLoader] Batching ${orderIds.length} shipping info requests`);
4
5 const all = await Promise.all(
6 orderIds.map(id => shippingDataSource.getShippingInfo(id))
7 );
8
9 // "sort by status so delivered ones come first" — looks harmless
10 return all.sort((a, b) => a.status.localeCompare(b.status));
11 });
12}
第 2 步 · 这是什么类型的错误Step 2 · what kind of error is this