Skip to content

第五章练习答案 ​

答案 05-01:紧急备注与非紧急排序 ​

完整 src/App.jsx:

jsx
import { memo, useState, useTransition } from 'react'

const names = Array.from({ length: 300 }, (_, i) =>
  `Name ${String(i).padStart(4, '0')}`,
)

function SlowRow({ name }) {
  const start = performance.now()
  while (performance.now() - start < 1) {} // 只模拟每项渲染成本
  return <li>{name}</li>
}

const SlowFirst = memo(function SlowFirst({ mode }) {
  const copy = [...names]
  copy.sort((a, b) => mode === 'name' ? a.localeCompare(b) : b.localeCompare(a))
  return <section><p>首项:{copy[0]}</p>
    <ul>{copy.map(name => <SlowRow key={name} name={name} />)}</ul>
  </section>
})

export default function App() {
  const [note, setNote] = useState('')
  const [mode, setMode] = useState('name')
  const [isPending, startTransition] = useTransition()

  function chooseMode(next) {
    startTransition(() => setMode(next))
  }

  return (
    <main>
      <label>
        紧急备注
        <input value={note} onChange={(event) => setNote(event.target.value)} />
      </label>
      <p>备注:{note}</p>
      <button disabled={isPending} onClick={() => chooseMode('name')}>升序</button>
      <button disabled={isPending} onClick={() => chooseMode('name-desc')}>降序</button>
      <p>{isPending ? '排序中' : `模式:${mode}`}</p>
      <SlowFirst mode={mode} />
    </main>
  )
}

观察:升序首项为 Name 0000,降序为 Name 0299;连续点同一模式仍相同;过渡期间备注更新能优先提交。复制 names 排序保证模块级数据不被修改;Transition 只包可延后的 mode。排序本身仍是不可中断的同步函数,本例用小数组控制它的成本,将慢工作放到各个 Row;不能声称 Transition 能打断 sort 的比较器。memo 让备注变化而 mode 不变时跳过慢子树。

答案 05-02:设置页的等待与失败边界 ​

src/App.jsx:

jsx
import { Component, Suspense, lazy, useState } from 'react'

let allowSettings
const ready = new Promise(resolve => { allowSettings = resolve })
const Settings = lazy(() => ready.then(() => import('./Settings.jsx')))

class SettingsBoundary extends Component {
  state = { error: null }
  static getDerivedStateFromError(error) { return { error } }
  componentDidUpdate(previousProps) {
    if (previousProps.resetKey !== this.props.resetKey && this.state.error) {
      this.setState({ error: null })
    }
  }
  render() {
    return this.state.error
      ? <p role="alert">{this.state.error.message}</p>
      : this.props.children
  }
}

export default function App() {
  const [page, setPage] = useState('home')
  const [mode, setMode] = useState('normal')
  return (
    <main>
      <button onClick={() => setPage('home')}>首页</button>
      <button onClick={() => setPage('settings')}>设置</button>
      <button onClick={() => allowSettings()}>允许设置模块加载</button>
      <select value={mode} onChange={(event) => setMode(event.target.value)}>
        <option value="normal">正常</option>
        <option value="broken">失败</option>
      </select>
      <SettingsBoundary resetKey={`${page}-${mode}`}>
        <Suspense fallback={<p>正在下载设置…</p>}>
          {page === 'home' ? <p>首页可用</p> : <Settings mode={mode} />}
        </Suspense>
      </SettingsBoundary>
    </main>
  )
}

src/Settings.jsx:

jsx
export default function Settings({ mode }) {
  if (mode === 'broken') throw new Error('settings unavailable')
  return <p>设置已加载</p>
}

检查:刷新后先打开设置,未按“允许设置模块加载”前一直显示指定 fallback;放行后才请求并显示模块。失败模式精确显示 settings unavailable;回首页、选正常再打开可恢复。等待由 lazy 的模块 Promise 触发,渲染错误由 Error Boundary 处理。闸门和 lazy 都只完成一次,重做首次等待需刷新;真实版本可以去掉闸门,直接 import,但慢网不是唯一验证手段。

答案 05-03:延迟书名筛选 ​

完整 src/App.jsx:

jsx
import { memo, useDeferredValue, useState } from 'react'

const books = ['Designing Data-Intensive Applications', 'React Explained', 'Eloquent JavaScript']

export function matchBooks(items, query) {
  const normalized = query.trim().toLowerCase()
  return items.filter((book) => book.toLowerCase().includes(normalized))
}

const Results = memo(function Results({ query }) {
  const matches = matchBooks(books, query)
  return <p>{matches.join(', ') || '无结果'}</p>
})

export default function App() {
  const [query, setQuery] = useState('')
  const deferredQuery = useDeferredValue(query)
  const stale = query !== deferredQuery
  return (
    <main>
      <input value={query} onChange={(event) => setQuery(event.target.value)} />
      <p>typed: {query}</p>
      <section aria-busy={stale}>
        <p>shown: {deferredQuery}</p>
        {stale && <p>仍显示旧结果</p>}
        <Results query={deferredQuery} />
      </section>
    </main>
  )
}

把下列断言临时放在模块末尾即可验证纯函数:

js
if (matchBooks(books, '').length !== 3) throw new Error('空查询应返回全部')
if (matchBooks(books, '  REACT ').join('') !== 'React Explained') {
  throw new Error('应忽略首尾空格和大小写')
}
if (matchBooks(books, 'rust').length !== 0) throw new Error('无匹配应为空')

UI 观察:两值不同时 aria-busy 为 true,追上后为 false。这里只有三本书,渲染很快,typed 领先 shown 可能短到肉眼看不到;不能把“没看见延迟”当作失败。需要观察调度时复用正文的逐行 SlowRow 与足够条目,不要在 matchBooks 中塞一个大循环。这个实现没有网络请求,因此也没有暗示延迟值会防抖请求。

返回第五章导读。

章节入口核对 ​

以下入口对应本章概览或综合练习,答案核对沿用本章各知识点的实现与日志。

用心记录每一步成长