5.3 KiB
5.3 KiB
id, title, type, domain, stack, tags, level, prerequisites, est_hours, related, week, order
| id | title | type | domain | stack | tags | level | prerequisites | est_hours | related | week | order | |||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| frontend.react.hooks-rendering | React 渲染机制、Hooks 规则、状态管理和 React 19 新特性 | lesson | frontend |
|
|
intermediate |
|
3 |
|
|
1 |
React 渲染机制、Hooks 规则、状态管理和 React 19 新特性
基于 React 19 编写,以官方文档为准:https://zh-hans.react.dev/ 。如果你主要用 Vue,这一课帮你建立 React 的心智模型,面试时能对比两者。
解释说明
渲染机制
- 组件每次渲染都会重新执行函数体。状态变化(
setState)、父组件渲染、Context 变化都会触发渲染。 - 渲染 → 生成新的元素树 → 和上一次比较(协调,reconciliation)→ 提交最小的 DOM 变更。列表的
key决定元素的身份。 - 状态更新是批量的(React 18 起在 Promise、定时器中也自动批量)。
- 状态是快照:在一次渲染中读取的 state 是固定的,
setCount(count + 1)连续调用三次只加 1,要用函数式更新setCount(c => c + 1)。
Hooks 规则和常见坑
| Hook | 要点 |
|---|---|
useState / useReducer |
复杂状态转换用 reducer,便于测试(见示例) |
useEffect |
用于与外部系统同步(订阅、请求、DOM 操作),返回清理函数;依赖数组要完整;开发模式下严格模式会执行“挂载 → 卸载 → 挂载”帮助发现缺少清理的问题 |
useMemo / useCallback / memo |
性能优化手段,不是语义保证;React Compiler 可以自动做记忆化(以官方文档为准) |
useRef |
保存不触发渲染的可变值、DOM 引用 |
| 自定义 Hook | 复用有状态逻辑;必须以 use 开头,只在顶层调用 |
规则:只在组件或自定义 Hook 的顶层调用 Hook,不能放在条件、循环里——因为 React 按调用顺序识别每个 Hook 的状态。
很多 useEffect 是不需要的:从 props 计算出来的值直接在渲染中计算;用户事件引起的逻辑放在事件处理函数里。
状态管理选择
局部状态 useState;跨组件共享且变化不频繁用 Context;复杂的客户端状态用 Zustand / Redux Toolkit;服务端数据(请求、缓存、重试、失效)用 TanStack Query 等,而不是放进全局 store。
React 19 和并发特性(了解)
- 并发特性:
useTransition、useDeferredValue把非紧急更新标记为可中断,保持输入流畅。 - React 19:Actions(异步函数作为
action,配合useActionState、useOptimistic处理表单提交、待定状态和乐观更新)、use读取 Promise / Context、ref可以作为普通 prop 传递。 - 服务端组件(Server Components)主要通过 Next.js 等框架使用,见 Next.js 课程。
案例
报表 Agent 的任务面板:任务有排队、运行、等待输入、成功、失败、取消等状态,服务端通过 SSE 推送事件,网络重连后可能收到重复或乱序的事件。用 useReducer 集中管理状态转换,事件带序号,旧事件和终态之后的事件直接丢弃;订阅在 useEffect 中建立并在任务切换 / 卸载时取消。
代码示例
reducer(完整版本 examples/task-panel/taskReducer.ts,node taskReducer-demo.ts 自检):
export function taskReducer(s: TaskState, e: TaskEvent): TaskState {
switch (e.type) {
case "submitted": return { ...initialState, id: e.id, status: "queued" };
case "cancel": return FINAL.has(s.status) ? s : { ...s, status: "cancelled" };
case "retry": return s.status === "failed" ? { ...s, status: "queued", error: null } : s;
default:
if (FINAL.has(s.status) || e.seq <= s.version) return s; // 丢弃重复、乱序、迟到的事件
}
...
}
组件(TaskPanel.tsx,严格模式 tsc 检查通过):
const [state, dispatch] = useReducer(taskReducer, initialState);
useEffect(() => {
dispatch({ type: "submitted", id: taskId });
const unsubscribe = subscribe(taskId, dispatch); // dispatch 引用稳定
return unsubscribe; // taskId 变化或卸载时取消订阅
}, [taskId, subscribe]);
易错点
useEffect依赖数组漏写,读到陈旧的值;或者为了“只执行一次”故意写空数组。- 订阅、定时器、请求没有清理,切换页面后旧数据覆盖新数据(竞态)。
- 在渲染中直接修改 state 对象(
state.list.push(x)),引用没变,不触发渲染。 - 用数组下标作为可重排列表的
key。 - 父组件每次渲染都创建新的对象 / 函数作为 props,导致
memo子组件失效。 - 把服务端数据放进 Redux,自己处理缓存、失效、重试,代码复杂且容易出错。
面试常问
- React 组件什么时候会重新渲染?怎么避免不必要的渲染?
- 为什么 Hooks 不能写在条件语句里?
useEffect的执行时机和清理时机?- React 和 Vue 的响应式 / 更新机制有什么区别?