This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/REACT/1. 基础篇/04-State 与不可变性.md
T
2026-04-29 22:19:51 +08:00

9.8 KiB
Raw Blame History

tags, create time
tags create time
React
State
Immutability
Hooks
Frontend
2026-04-29 22:03

State 与不可变性

概述

State 是 React 组件的数据源。不可变性(Immutability)是 React 状态管理的基石——React 通过引用比较判断 state 是否变化,只有创建了新的引用才会触发重渲染。理解这一点,就能避免 "数据变了但 UI 没更新" 这一经典 Bug。

为什么必须遵守不可变性?

graph LR
    A["setState(newState)"] --> B{"old === new?"}
    B -->|不同| C["触发重渲染 ✅"]
    B -->|相同| D["跳过渲染 ❌ 数据已变但 UI 不变"]

    style C fill:#4FC08D,color:#fff
    style D fill:#F5A87D,color:#000

[!question] 思考:为什么 React 用浅比较而不是深比较?

  • 性能:深比较复杂度为 O(n),大型嵌套对象代价极高;浅比较 O(1)
  • 职责划分:React 负责高效检测变化,开发者负责正确生成新引用
  • 历史教训:Redux / MobX 等库的对比证明,强制不可变性让调试和追踪变更成为可能

[!note] 核心原则 React 只认引用地址,不认内容。{a: 1} 和 {a: 1} 在 JS 中是两个不同的对象,=== 为 false。因此只要每次创建新引用,React 就会检测到变化。

useState 基础用法

const [count, setCount] = useState(0);
const [user, setUser] = useState<{ name: string; age: number }>({ name: "", age: 0 });
const [items, setItems] = useState<string[]>([]);

// setState 可以接收值或 updater 函数
setCount(c => c + 1);  // 推荐:基于旧值时用 updater,避免闭包陷阱

Updater 函数模式

当新 state 依赖旧 state 时,必须使用 updater 形式:

// 场景:快速连续点击,用值可能导致丢失更新
handleClick = () => {
  setCount(count + 1);  // ❌ 如果 count 被缓存,可能只+1
  setCount(count + 1);
};

handleClick = () => {
  setCount(c => c + 1);  // ✅ 每次都拿到最新值,两次点击正确 +2
  setCount(c => c + 1);
};

[!tip] 最佳实践 养成习惯:凡是新值依赖于旧值,一律用 updater 函数。即使当前看起来没问题,也能预防未来重构引入的闭包问题。

不可变更新模式

数组操作

const [list, setList] = useState(["a", "b", "c"]);

// 添加末尾
setList(prev => [...prev, "d"]);

// 插入开头
setList(prev => ["new", ...prev]);

// 删除(按 index)
setList(prev => prev.filter((_, i) => i !== targetIndex));

// 更新指定元素
setList(prev => prev.map((item, i) =>
  i === targetIndex ? { ...item, completed: true } : item
));

// 安全排序(sort() 原地修改!必须先拷贝)
setList(prev => [...prev].sort((a, b) => a.id - b.id));

[!warning] 常见陷阱

// ❌ sort()/reverse()/push()/splice() 都原地修改原数组!
list.sort();         // list 引用不变
setList(list);       // React 跳过渲染

// ✅ 先拷贝再操作
setList([...list].sort());

对象更新

const [form, setForm] = useState({
  profile: { name: "", email: "" },
  settings: { theme: "light", notifications: true },
});

// 顶层字段 —— 展开一层即可
setForm(prev => ({ ...prev, title: "新标题" }));

// 嵌套一层 —— 逐层展开
setForm(prev => ({
  ...prev,
  profile: { ...prev.profile, name: "新名字" },
}));

[!warning] 多层嵌套会非常冗长,这是手动管理复杂 state 的天然缺陷

如果你的嵌套超过两层,说明应该考虑 useReducer 或外部状态管理库了。

状态提升

[!abstract] 什么是状态提升? React 官方提出的设计模式:将共享状态移动到最先需要它的共同父组件中,子组件通过 props 读取,通过回调 prop 请求变更。

flowchart TD
    subgraph A["状态提升前 ❌"]
        S1[CounterA 独立 state]
        S2[CounterB 独立 state]
        S3[总和无法计算]
        S1 --> S3
        S2 --> S3
    end

    subgraph B["状态提升后 ✅"]
        P[App 持有 state]
        C1[CounterA via props]
        C2[CounterB via props]
        Total["显示总和"]
        P --> C1
        P --> C2
        P --> Total
    end
// 状态提升后
function App() {
  const [countA, setCountA] = useState(0);
  const [countB, setCountB] = useState(0);

  return (
    <>
      <p>总和: {countA + countB}</p>
      <Counter value={countA} onChange={setCountA} />
      <Counter value={countB} onChange={setCountB} />
    </>
  );
}

[!info] 何时使用状态提升?

  • 两个或多个组件需要同步同一份数据
  • 一个组件的交互需要影响其他组件的行为
  • 没有更好的替代方案(如 Context、状态管理库)之前,先从状态提升开始

useReducer:管理复杂 state

当 state 逻辑变复杂时,useReducer 比 useState(updater) 更清晰:

interface State {
  items: Item[];
  status: "idle" | "loading" | "error";
  error?: string;
}
type Action =
  | { type: "ADD"; payload: Item }
  | { type: "SET_LOADING" }
  | { type: "SET_ERROR"; payload: string };

function reducer(state: State, action: Action): State {
  switch (action.type) {
    case "ADD":       return { ...state, items: [...state.items, action.payload] };
    case "SET_LOADING": return { ...state, status: "loading" };
    case "SET_ERROR":   return { ...state, status: "error", error: action.payload };
    default:           return state;  // 未匹配的动作返回原 state
  }
}

function MyComponent() {
  const [state, dispatch] = useReducer(reducer, initialState);

  return (
    <div>
      {state.status === "loading" && <Spinner />}
      {state.status === "error" && <Alert>{state.error}</Alert>}
      {state.items.map(item => (
        <Item key={item.id} {...item} onRemove={() => dispatch({ type: "REMOVE", payload: item.id })} />
      ))}
    </div>
  );
}

[!tip] useReducer 的优势

  1. 动作语义化:dispatch({ type: "ADD" }) 比 setState() 更能表达意图
  2. 逻辑集中:所有变更规则集中在 reducer 函数里,方便测试
  3. 批量关联更新:一个 action 可以同时更新多个相互关联的子值
  4. DevTools 友好:配合 Redux DevTools 可实现时间旅行调试

useState vs useReducer 决策表

维度 选 useState 选 useReducer
state 类型 简单标量(number, boolean, string) object / array,结构较复杂
下一个 state 不依赖旧值,或直接给出 依赖旧值,且有分支逻辑
变更来源 单一触发点 多个可能的 action 类型
子值联动 无需联动 一次操作需更新多个子值
可测试性 不需要单独测试 reducer 是纯函数,可独立测试

[!example] 同一个场景的两种写法对比

假设实现一个计数器,支持 increment/decrement/reset:

// useState 版本
const [n, setN] = useState(0);
// reset 需要知道初始值,不够自包含

// useReducer 版本
function reducer(state, action) {
  switch (action.type) {
    case "INCREMENT": return { n: state.n + 1 };
    case "DECREMENT": return { n: state.n - 1 };
    case "RESET":     return { n: INITIAL_VALUE };  // self-contained
  }
}

什么时候不应该用 State?

[!important] 误区:把所有数据都塞进 state

不是所有数据都需要 state。以下情况应优先考虑替代方案:

// ❌ 不该存为 state:可以从 props 或其他 state 计算得出
const [fullName, setFullName] = useState("");
useEffect(() => setFullName(`${firstName} ${lastName}`), [firstName, lastName]);

// ✅ 直接在渲染中计算
const fullName = `${firstName} ${lastName}`;
import { useRef } from "react";

// ❌ 不该存为 state:需要跨渲染保持可变引用(定时器 ID、DOM 节点、WebSocket)
let timerId = useRef<NodeJS.Timeout>();  // 不能在渲染期间赋值

// ✅ 用 useRef 存储非渲染相关的可变值
const timerRef = useRef<NodeJS.Timeout>();
timerRef.current = setTimeout(() => {}, 1000);

// ⚠️ 注意:修改 ref.current 不会触发重渲染!
import { useMemo, useCallback } from "react";

// ❌ 过度优化:每个小值都用 useMemo/useCallback
const [n, setN] = useState(0);
const doubled = useMemo(() => n * 2, [n]);  // 多此一举

// ✅ useMemo/useCallback 只在昂贵计算或传给 memo 子组件时使用

性能注意事项

[!summary] 本节要点:state 的每一次 setter 调用都会触发当前组件重渲染,优化的核心思路是减少不必要的渲染和降低渲染成本。

  1. 拆分 state — 不相关的状态不要放在同一个 state 变量里,避免一处变更导致整个大对象所在组件全部重渲染

  2. 惰性初始化 — useState(() => computeExpensiveValue()) 中的工厂函数只在首次挂载时执行,适合初始化开销大的场景

  3. 稳定引用 — 组件函数本身在每次渲染都是新引用,如需传给子组件,优先用 useCallback;跨渲染保存可变引用用 useRef

// 惰性初始化
const [cache, setCache] = useState<Map<string, any>>(() => new Map());

// useRef 存储 callback(配合 memo 使用)
const callbackRef = useRef(callback);
callbackRef.current = callback;
// 子组件中通过 callbackRef.current 访问

[!tip] React 18 批处理升级

React 18 起,所有状态更新都自动批处理,包括:

  • Promise 回调内的 setState
  • setTimeout 内的 setState
  • 原生事件处理器内的 setState

不再需要 unstable_batchedUpdates,也不再需要担心手动批处理的问题。

关联笔记

  • hhs/REACT/1. 基础篇/03-组件与 Props
  • hhs/REACT/2. Hooks 篇/05-核心 Hooks
  • hhs/REACT/2. Hooks 篇/06-性能优化 Hooks
  • hhs/REACT/4. 进阶篇/11-组件通信模式
  • hhs/REACT/5. 工程实践篇/15-性能优化