Files
cs-note/hhs/REACT/1. 基础篇/04-State 与不可变性.md
T
2026-05-24 11:42:38 +08:00

318 lines
9.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [React, State, Immutability, Hooks, Frontend]
create time: 2026-04-29 22:03
---
# State 与不可变性
## 概述
State 是 React 组件的数据源。**不可变性**(Immutability)是 React 状态管理的基石——React 通过引用比较判断 state 是否变化,只有创建了新的引用才会触发重渲染。理解这一点,就能避免 "数据变了但 UI 没更新" 这一经典 Bug。
## 为什么必须遵守不可变性?
```mermaid
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 基础用法
```tsx
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 形式:
```tsx
// 场景:快速连续点击,用值可能导致丢失更新
handleClick = () => {
setCount(count + 1); // ❌ 如果 count 被缓存,可能只+1
setCount(count + 1);
};
handleClick = () => {
setCount(c => c + 1); // ✅ 每次都拿到最新值,两次点击正确 +2
setCount(c => c + 1);
};
```
> [!tip] 最佳实践
> 养成习惯:**凡是新值依赖于旧值,一律用 updater 函数**。即使当前看起来没问题,也能预防未来重构引入的闭包问题。
## 不可变更新模式
### 数组操作
```tsx
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] 常见陷阱
>
> ```tsx
> // ❌ sort()/reverse()/push()/splice() 都原地修改原数组!
> list.sort(); // list 引用不变
> setList(list); // React 跳过渲染
>
> // ✅ 先拷贝再操作
> setList([...list].sort());
> ```
### 对象更新
```tsx
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 请求变更。
```mermaid
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
```
```tsx
// 状态提升后
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)` 更清晰:
```tsx
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:
>
> ```tsx
> // 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。以下情况应优先考虑替代方案:
```tsx
// ❌ 不该存为 state:可以从 props 或其他 state 计算得出
const [fullName, setFullName] = useState("");
useEffect(() => setFullName(`${firstName} ${lastName}`), [firstName, lastName]);
// ✅ 直接在渲染中计算
const fullName = `${firstName} ${lastName}`;
```
```tsx
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 不会触发重渲染!
```
```tsx
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`
```tsx
// 惰性初始化
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-性能优化]]