Skip to content

第二章练习答案 ​

每题代码分别替换 src/App.jsx。断言失败会直接抛错,避免 console.assert 只打印消息后仍让验证看似成功。

答案 01:比分回合队列 ​

jsx
import { useState } from 'react'

export default function App() {
  const [score, setScore] = useState(10)
  const [rounds, setRounds] = useState([])

  function addThree() {
    setScore(value => value + 1)
    setScore(value => value + 1)
    setScore(value => value + 1)
  }

  function applyPenalty() {
    setScore(20)
    setScore(value => value + 2)
  }

  function recordRound() {
    setRounds(current => [...current, score])
    setScore(value => value + 1)
  }

  return (
    <main>
      <h1>比分:{score}</h1>
      <button onClick={addThree}>罚球三中</button>
      <button onClick={applyPenalty}>判罚改为 20 后加 2</button>
      <button onClick={recordRound}>记录本回合</button>
      <ol>{rounds.map((value, index) => <li key={index}>{value}</li>)}</ol>
    </main>
  )
}

从 10 点击“罚球三中”得到 13:队列是 10→11→12→13。判罚按钮不依赖旧快照,先替换为 20,更新函数再收到 20 并得到 22。记录按钮中的 score 是该次渲染快照,所以从 22 点击后,日志追加 22,下一次界面显示 23。

答案 02:库存批次更新函数 ​

jsx
import { useState } from 'react'

const initialItems = [
  { id: 'tea', name: '茶叶', batches: [
    { id: 'T-1', quantity: 4 },
    { id: 'T-2', quantity: 7 },
  ] },
  { id: 'cup', name: '杯子', batches: [{ id: 'C-1', quantity: 3 }] },
]

export function updateBatch(items, itemId, batchId, quantity) {
  if (!Number.isInteger(quantity) || quantity < 0) return items

  const itemIndex = items.findIndex(item => item.id === itemId)
  if (itemIndex === -1) return items
  const batchIndex = items[itemIndex].batches.findIndex(batch => batch.id === batchId)
  if (batchIndex === -1) return items

  const oldItem = items[itemIndex]
  const newBatches = oldItem.batches.map(batch => (
    batch.id === batchId ? { ...batch, quantity } : batch
  ))
  return items.map(item => (
    item.id === itemId ? { ...item, batches: newBatches } : item
  ))
}

function check(condition, message) {
  if (!condition) throw new Error(message)
}

const changed = updateBatch(initialItems, 'tea', 'T-1', 9)
check(changed !== initialItems, '外层数组应更新')
check(changed[0] !== initialItems[0], '目标商品应更新')
check(changed[0].batches[0] !== initialItems[0].batches[0], '目标批次应更新')
check(changed[0].batches[1] === initialItems[0].batches[1], '其他批次应复用')
check(changed[1] === initialItems[1], '其他商品应复用')
check(initialItems[0].batches[0].quantity === 4, '旧输入不可被修改')
check(updateBatch(initialItems, 'missing', 'T-1', 9) === initialItems, '未找到应原样返回')
check(updateBatch(initialItems, 'tea', 'missing', 9) === initialItems, '未找到批次应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', -1) === initialItems, '非法数量应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', 1.5) === initialItems, '小数应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', NaN) === initialItems, 'NaN 应原样返回')
check(updateBatch(initialItems, 'tea', 'T-1', 0)[0].batches[0].quantity === 0, '零库存是合法更新')

export default function App() {
  const [items, setItems] = useState(initialItems)

  return (
    <main>
      <h1>批次库存</h1>
      {items.map(item => (
        <section key={item.id}>
          <h2>{item.name}</h2>
          {item.batches.map(batch => (
            <label key={batch.id}>
              {batch.id}
              <input
                type="number"
                min="0"
                step="1"
                value={batch.quantity}
                onChange={event => {
                  const quantity = Number(event.target.value)
                  setItems(current => updateBatch(current, item.id, batch.id, quantity))
                }}
              />
            </label>
          ))}
        </section>
      ))}
    </main>
  )
}

先确认商品和批次存在,才能判断是否需要新引用;否则 map 即使找不到目标也会无意义地产生新数组。断言分别覆盖外层、变化路径、未变化分支、旧值和失败分支,能排除“深拷贝一切”和“只复制外层”两类错误实现。

答案 03:按项目隔离的搜索草稿 ​

jsx
import { useState } from 'react'

const projects = [
  { id: 'docs', name: '文档站', items: ['React 导读', '链接检查', '部署说明'] },
  { id: 'shop', name: '商店', items: ['商品页', '购物车', '结算页'] },
]

function SearchPanel({ project, query, onQueryChange }) {
  const normalized = query.trim().toLocaleLowerCase()
  const results = project.items.filter(item => (
    item.toLocaleLowerCase().includes(normalized)
  ))

  return (
    <section>
      <h2>{project.name}</h2>
      <input
        aria-label="搜索"
        value={query}
        onChange={event => onQueryChange(event.target.value)}
      />
      {results.length === 0 ? <p>无匹配项</p> : (
        <ul>{results.map(item => <li key={item}>{item}</li>)}</ul>
      )}
    </section>
  )
}

export default function App() {
  const [selectedId, setSelectedId] = useState(projects[0].id)
  const [drafts, setDrafts] = useState({})
  const selected = projects.find(project => project.id === selectedId)
  const query = drafts[selectedId] ?? ''

  function changeQuery(value) {
    setDrafts(current => ({ ...current, [selectedId]: value }))
  }

  return (
    <main>
      <h1>项目搜索</h1>
      {projects.map(project => (
        <button key={project.id} onClick={() => setSelectedId(project.id)}>
          {project.name}
        </button>
      ))}
      <SearchPanel project={selected} query={query} onQueryChange={changeQuery} />
    </main>
  )
}

验收:在文档站输入“链接”,切到商店输入“页”,再切回时分别恢复。results 每次由 project.items + query 计算,不存在需要同步的第三份 state;selected 也从稳定 id 查找,所以项目对象更新不会留下旧副本。

返回第二章导读。

用心记录每一步成长