Kanban 看板:一次改两个数组Kanban board: changing two arrays at once
题面The problem
先把要求读完,再动手。Read every requirement before you start.
一次操作同时改两个数组,而且不许碰原 board。 检查器会查两个边界和「未动的列复用引用」。
One action changes two arrays, and the board you were handed must not be touched. The checker looks at the two edge cases and at whether untouched columns keep their original reference.
- from === to 时返回同一个引用,不造新对象When from === to, return the same reference and build no new object
- 找不到卡时返回同一个引用When the card is not found, return the same reference
- 源列用 filter 去掉,目标列用展开追加Drop it from the source column with filter, and append to the target column with spread
- 只改这两列,其余列复用原数组Only those two columns change; the rest keep their original arrays
- 不许 push / splice / 直接赋值No push, no splice, no direct assignment
预计 40 分钟。这个数字是照「读完题就开始写、不查资料」估的 —— 第一次超时很正常,第二次要压进去。Budget 40 minutes. Overrunning on the first pass is normal; the second pass should fit.
工作区Workspace
工作区是一个真的浏览器沙箱:左边写代码,右边实时预览,下面一个「跑测试」按钮。测试和本机那套是同一批断言,转写成了浏览器里能跑的写法。The workspace is a real in-browser sandbox: edit on the left, live preview on the right, one Run button below. The assertions are the same ones that pass on a real machine, rewritten for the browser runner.
需要联网。Requires an internet connection. 打包器和 npm 依赖都在 CodeSandbox 的远程服务上(评估过程见 docs/sandpack-evaluation.md),断网这块就起不来 —— 那就照下面的命令在本机跑。The bundler and the npm packages come from CodeSandbox's remote service, so this panel needs network access.
展开讲解Walkthrough
下面是《缺口四 · Kanban 看板:一次改两个数组》那一节的原文 —— 和课程里是同一份内容,不是另写的摘要。卡住了再展开。Below is the actual text of the lesson “缺口四 · Kanban 看板:一次改两个数组” — the same content as in the course, not a rewritten summary. Expand it when you stall.
展开《缺口四 · Kanban 看板:一次改两个数组》(2 段 · 约 20 分钟)Expand “缺口四 · Kanban 看板:一次改两个数组” (2 sections · ~20 min)
完整那一节(含练习、常见错误、迁移模式)在The full lesson (with exercises, common mistakes and transfer patterns) is at 缺口四 · Kanban 看板:一次改两个数组。
数据形状:用 Record 而不是数组套数组The shape of the data: a Record, not an array of arrays
board 是「列 id → 卡片数组」的映射。The board maps a column id to an array of cards.
Record<ColumnId, Card[]>比 Column[](每个 column 里有 cards)好用, 因为按列 id 直接取, 不用先 find 找到列再改。
而且展开语法配计算属性名 正好能一次改两个键:{ ...board, [from]: …, [to]: … }。 如果是数组套数组, 同样的事要写两次 map, 明显更绕。
列的顺序单独放一个常量数组(COLUMNS), 因为「顺序」是展示逻辑, 不该混进数据结构里 —— 这样加一列只改一处。
Record<ColumnId, Card[]> is easier to work with than Column[] (where each column holds its own cards), because you index straight by column id instead of running find to locate the column first.
And spread syntax plus computed property names changes two keys in one go: { ...board, [from]: ..., [to]: ... }. With arrays inside arrays the same thing takes two map calls, clearly more roundabout.
Keep the column order in its own constant array (COLUMNS), because “order” is display logic and should not be mixed into the data structure — that way adding a column touches one place.
moveCard:这道题的全部难点moveCard: the whole difficulty of this problem
函数体十行,四个关键决定。每一个都有理由。Ten lines of code, four decisions that matter. Each one has a reason.
逐行读:
if (from === to) return board——返回原引用, React 直接跳过重渲染。 写成return { ...board }会白渲染一次。find找不到也原样返回—— 别抛错,也别返回半个 board。[from]: board[from].filter(...)—— 源列去掉,新数组。[to]: [...board[to], card]—— 目标列追加,新数组。
关键性质:{ ...board }只浅拷贝顶层, 所以没被列出来的列(比如done)复用的是原来那个数组引用。 这正是我们想要的 —— 和评论树那道题「只重建路径上的节点」 是完全同一个思路。
测试里专门验证了这三件事: 原 board 深冻结后调用不抛错、next.done === b.done(未动的复用)、next.todo !== b.todo(动过的是新的)。
为什么必须是纯函数、不能在组件里写:
- 可以脱离 React 单测—— 六条测试里三条根本不渲染组件。
- 不可能产生中间态。 写成「先
setBoard删、 再setBoard加」, 虽然 React 18 会批处理, 但一旦中间插入任何提前return或校验, 卡片就凭空消失了。 - 以后要接真拖拽、撤销、 或者服务端同步,这个函数原样能用。
Line by line:
if (from === to) return board— return the original reference and React skips the re-render outright. Writingreturn { ...board }costs you one wasted render.- When
findcomes up empty, return the board as it is — do not throw, and do not return half a board. [from]: board[from].filter(...)— the source column drops it, new array.[to]: [...board[to], card]— the target column appends, new array.
The key property: { ...board } only copies the top level, so columns you did not list (say done) reuse the array reference they already had. That is exactly what we want, and exactly the same idea as “rebuild only the nodes on the path” in the comment-tree question.
The test verifies these three things: calling it with a deep-frozen original board does not throw, next.done === b.done (untouched columns are reused), and next.todo !== b.todo (touched ones are new).
Why it has to be a pure function instead of living inside the component:
- It can be unit tested without React — three of the six tests never render a component.
- An in-between state becomes impossible. Write it as “
setBoardto delete, thensetBoardto add” and, even though React 18 batches, the moment any earlyreturnor validation slips in between, the card vanishes. - Later, when you wire up real drag and drop, undo, or server sync, this function works unchanged.
参考答案Reference solution
提示是一级一级放的。四级看完还写不出来,再开答案门。The hints come one level at a time. If all four leave you stuck, open the answer.
这份答案在本机真跑过测试。但先确认你自己动手写过一遍 —— 读懂答案和写出答案是两种能力,考场上考的是后一种。This answer really was run here and its tests passed. But write it yourself first — reading an answer and producing one are two different skills, and the exam tests the second.