Files

5.3 KiB
Raw Permalink Blame History

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
react
React
Hooks
useEffect
useReducer
渲染
memo
并发特性
React 19
React Compiler
intermediate
frontend.typescript.type-system
3
frontend.react.scenario.long-task-ui
frontend.ssr.nextjs-app-router
frontend.react.interview
7
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 的响应式 / 更新机制有什么区别?