Files
cs-note/hhs/REACT/1. 基础篇/04-State 与不可变性.md
T

318 lines
9.8 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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-性能优化]]