第五章练习答案
答案 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 中塞一个大循环。这个实现没有网络请求,因此也没有暗示延迟值会防抖请求。
返回第五章导读。
章节入口核对
以下入口对应本章概览或综合练习,答案核对沿用本章各知识点的实现与日志。