主题
第 12 章 · React 进阶
一句话开篇:理解 Hooks 的高级用法 + 性能优化 + 虚拟 DOM/Fiber 原理,从"会用 React"晋级为"能讲明白 React"。
0. 生活类比
类比一:自定义 Hooks = 自配的厨房工具包
useState useEffect 是 React 给你的标准锅碗瓢盆,但日常做饭你会发现"切水果 + 装盘 + 拍照"这套动作总是一起出现。自定义 Hook 就是把这套动作打包成一个 useFruitPlate 工具,下次直接拿来用,不用重写。
类比二:Fiber 架构 = 厨师的"工单管理系统"
老的 React Reconciler 像"老厨师":菜单一来就闷头炒,必须做完才能停。客人插队(用户输入)只能干等。
Fiber 像"现代化厨房工单系统":把大菜单拆成一个个工单(Fiber 节点),中间可以暂停、看看有没有 VIP 插队(高优先级任务),处理完急单再回来继续——这就是可中断渲染 + 时间切片。
类比三:虚拟 DOM Diff = 装修对比图
老房子(旧虚拟 DOM)和新设计稿(新虚拟 DOM)摆一起对比:
- 同一位置类型不同(沙发变成床)→ 直接拆掉重装
- 同一位置同类型(沙发换了颜色)→ 只换布套
- 多个相同位置(一排椅子)→ 看每个椅子的"标签"(key)来识别
1. 自定义 Hooks
是什么?
自定义 Hook 是一个以 use 开头的函数,内部可以调用其他 Hooks,把"组件中重复的状态逻辑"抽离出来复用。
例子:useToggle(开关)
jsx
function useToggle(initial = false) {
const [on, setOn] = useState(initial);
const toggle = useCallback(() => setOn(o => !o), []);
return [on, toggle];
}
// 使用
function App() {
const [show, toggleShow] = useToggle(false);
return (
<>
<button onClick={toggleShow}>{show ? '隐藏' : '显示'}</button>
{show && <p>Hi</p>}
</>
);
}例子:useFetch(数据请求)
jsx
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => !cancelled && (setData(d), setLoading(false)))
.catch(e => !cancelled && (setError(e), setLoading(false)));
return () => { cancelled = true; };
}, [url]);
return { data, loading, error };
}自定义 Hook 三原则
- 必须以
use开头(ESLint 才能识别成 Hook) - 遵守 Hooks 规则(只能在顶层、组件/Hook 中调用)
- 返回值无强制约定(数组、对象、单值都可以)
2. useMemo 和 useCallback —— 缓存
共同点:都用来避免重复计算/重复创建。区别:
useMemo缓存"计算结果",useCallback缓存"函数引用"。
useMemo
jsx
const expensive = useMemo(() => {
return list.filter(...).sort(...).reduce(...); // 重计算
}, [list]); // 依赖不变就用上次结果useCallback
jsx
const handleClick = useCallback(() => {
console.log(id);
}, [id]); // 依赖不变就用同一个函数引用等价关系:
jsx
useCallback(fn, deps) === useMemo(() => fn, deps)useCallback 真正解决的问题
单独用 useCallback 没用! 它必须配合 React.memo 才有意义:
jsx
const Child = React.memo(({ onClick }) => {
console.log('Child render');
return <button onClick={onClick}>click</button>;
});
function Parent() {
const [n, setN] = useState(0);
// ❌ 每次渲染都新建 fn,Child 还是会重渲染
// const handleClick = () => console.log('click');
// ✅ 配合 memo,Child 不再重复渲染
const handleClick = useCallback(() => console.log('click'), []);
return (
<>
<button onClick={() => setN(n+1)}>{n}</button>
<Child onClick={handleClick} />
</>
);
}⚠️ 不要无脑用 memo / useMemo / useCallback
它们本身有缓存开销。只在以下情况使用:
- 计算成本高(大数组排序、复杂过滤)
- 子组件被
React.memo包裹,需要稳定引用 - 作为其他 Hook 的依赖项需要稳定
3. useReducer —— 复杂状态的"小型 Redux"
适合:多状态联动、状态更新逻辑复杂。
jsx
const initial = { count: 0, step: 1 };
function reducer(state, action) {
switch (action.type) {
case 'add': return { ...state, count: state.count + state.step };
case 'sub': return { ...state, count: state.count - state.step };
case 'step': return { ...state, step: action.payload };
case 'reset': return initial;
default: throw new Error('未知 action');
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initial);
return (
<>
<p>{state.count}(step {state.step})</p>
<button onClick={() => dispatch({ type: 'add' })}>+</button>
<button onClick={() => dispatch({ type: 'sub' })}>-</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'step', payload: +e.target.value })}
/>
<button onClick={() => dispatch({ type: 'reset' })}>重置</button>
</>
);
}useState vs useReducer
| 维度 | useState | useReducer |
|---|---|---|
| 适合场景 | 简单状态、独立字段 | 多字段联动、状态机 |
| 更新方式 | setX(val) 直接更新 | dispatch(action) 经过 reducer |
| 可测试性 | 一般 | 好(reducer 是纯函数,可单独测) |
| 调试 | console 看 setX | 可中央打印 action 流 |
| 与 Context 配合 | 也可以 | 配合 Context 实现"轻量 Redux" |
4. useContext —— 跨层级数据传递
解决什么问题?
Props Drilling(属性钻取):祖先要把数据传给孙子,必须层层透传。
App ──(theme)──> Layout ──(theme)──> Sidebar ──(theme)──> Avatar
↑
其实只有这里用中间组件被迫成为"传话筒"。
用 Context 直接打通
jsx
const ThemeContext = React.createContext('light');
function App() {
return (
<ThemeContext.Provider value="dark">
<Layout />
</ThemeContext.Provider>
);
}
function Avatar() {
const theme = useContext(ThemeContext); // 直接拿
return <div className={theme}>...</div>;
}Context + useReducer = 轻量 Redux
jsx
const CartContext = createContext();
function CartProvider({ children }) {
const [cart, dispatch] = useReducer(cartReducer, []);
return (
<CartContext.Provider value={{ cart, dispatch }}>
{children}
</CartContext.Provider>
);
}⚠️ Context 一变化,所有消费它的组件都会重渲染。频繁变化的状态用 Context 性能不佳,要么拆 Context,要么上 Redux/Zustand。
5. 性能优化全景
5.1 React.memo —— 浅比较 props
jsx
const Child = React.memo(({ name }) => <p>{name}</p>);
// props 浅比较相等 → 跳过渲染5.2 useMemo / useCallback —— 稳定引用
见第 2 节。
5.3 拆分组件 —— 把状态局部化
jsx
// ❌ 输入框输入会让整个 App 重渲染
function App() {
const [text, setText] = useState('');
return <>
<input value={text} onChange={e => setText(e.target.value)} />
<BigList /> {/* 也跟着 render */}
</>;
}
// ✅ 把输入下沉到子组件
function App() {
return <><Input /><BigList /></>;
}5.4 虚拟列表 —— 只渲染可视区
10000 条数据全渲染 = 卡爆。只渲染屏幕能看到的 ~20 条 + 上下缓冲:
全量数据:[0...9999]
┌─────────────────┐
│ 可视区域(20) │ ← 只渲染这段
└─────────────────┘
滚动 → 改变 startIndex → 重新渲染窗口主流方案:react-window / react-virtual。
5.5 懒加载
jsx
const Heavy = React.lazy(() => import('./Heavy'));
<Suspense fallback={<Loading />}>
<Heavy />
</Suspense>5.6 useTransition / useDeferredValue(React 18)
把"不紧急的更新"标记为低优先级:
jsx
const [isPending, startTransition] = useTransition();
const onChange = (e) => {
setInput(e.target.value); // 紧急(输入框立即响应)
startTransition(() => {
setList(filter(e.target.value)); // 不紧急(万级列表过滤)
});
};6. 虚拟 DOM 与 Diff 算法
虚拟 DOM 是什么?
是用 JS 对象描述真实 DOM 结构的轻量数据结构。React 通过对比新旧两棵虚拟 DOM 树,得出最小变更集,再批量应用到真实 DOM。
js
// <div className="box"><h1>Hi</h1></div>
const vdom = {
type: 'div',
props: {
className: 'box',
children: [{ type: 'h1', props: { children: 'Hi' } }]
}
};Diff 算法三大启发式
- 同层比较:只比较同层级节点,不跨层(O(n) 而非 O(n³))
- type 不同直接销毁重建:
<div>变<span>→ 不复用 - 列表用 key 标识:避免误判"修改"为"新增/删除"
旧:<ul> 新:<ul>
<li key=A> ↓ <li key=B> ← B 复用旧 B
<li key=B> <li key=A> ← A 复用旧 A
<li key=C> <li key=D> ← D 新建
</ul> </ul> ← C 删除Diff 复杂度
传统树 Diff 是 O(n³),React 通过上述三个假设把复杂度降到 O(n)。
7. Fiber 架构
为什么要 Fiber?(React 16 重构)
老 Reconciler 是递归的,一旦开始无法中断。如果组件树很大(几千节点),渲染会卡住主线程几百毫秒,用户输入毫无响应。
Fiber 是什么
把"虚拟 DOM 树"从"递归"变成链表:每个组件对应一个 Fiber 节点,节点之间通过 child sibling return 指针连接。
App (Fiber)
│ child
▼
Header ─sibling→ Main ─sibling→ Footer
│
▼ child
Card ─sibling→ Card ─sibling→ CardFiber 带来的能力
- 可中断:每处理一个 Fiber 节点就检查"还有时间吗",没时间就让出主线程,下次再继续
- 优先级调度:用户输入(高) > 网络数据(中) > 数据预取(低)
- 双缓存(current / workInProgress):渲染失败可以丢弃 wip 树,回到 current
- 副作用收集:先在内存中算好所有变更,再一次性提交(Commit Phase)
渲染两阶段(ASCII 图)
┌────────────────────────────────────────────────┐
│ Render Phase(可中断、可丢弃) │
│ - 构建/更新 Fiber 树 │
│ - 执行 reconcile(diff) │
│ - 标记需要变更的 effect tag │
│ 时间切片:每 5ms 检查一次有没有更高优先级 │
└────────────────────┬───────────────────────────┘
│ 完成(无中断)
▼
┌────────────────────────────────────────────────┐
│ Commit Phase(同步、不可中断) │
│ - 执行 DOM 操作 │
│ - 调用 useLayoutEffect / componentDidMount │
│ - 调度 useEffect │
└────────────────────────────────────────────────┘8. 受控组件 vs 非受控组件
| 维度 | 受控 | 非受控 |
|---|---|---|
| 数据源 | React state | DOM 自身 |
| 写法 | value + onChange | defaultValue + ref |
| 实时校验 | 容易 | 难 |
| 适合场景 | 大多数表单 | 集成第三方 DOM 库、文件上传 |
jsx
// 受控
<input value={name} onChange={e => setName(e.target.value)} />
// 非受控
<input defaultValue="" ref={inputRef} />9. forwardRef 与 useImperativeHandle
函数组件默认不能接收 ref。要用 forwardRef 转发:
jsx
const Input = forwardRef((props, ref) => (
<input ref={ref} {...props} />
));
// 父组件
const ref = useRef();
<Input ref={ref} />
ref.current.focus();自定义暴露的方法(封装内部细节)
jsx
const FancyInput = forwardRef((props, ref) => {
const inputRef = useRef();
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; }
}));
return <input ref={inputRef} />;
});10. Portal —— 渲染到组件树之外
jsx
ReactDOM.createPortal(
<Modal>...</Modal>,
document.body // 渲染到 body 下,逃出父容器的 overflow / z-index 限制
)典型场景:弹窗、Toast、Tooltip。事件冒泡按 React 树冒泡,不是按 DOM 树。
11. Suspense 入门
jsx
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>- 代码分割:配合
React.lazy异步加载组件 - 数据获取(实验/部分库支持):组件读取数据时如果还没加载好,"挂起"自身,让 Suspense 显示 fallback
12. ⚠️ 易踩的坑
- 滥用 useMemo / useCallback:本身有开销,没有 memo 接住时几乎无收益。
- memo 失效:传入了对象/数组/函数字面量 → 每次都是新引用,浅比较失败。
- Context 频繁更新引发全树重渲染:把"频繁变"和"很少变"的状态拆开放在两个 Context。
- 依赖数组里放对象/数组:
[obj]每次都是新引用 → effect 一直触发。要么拆成原始值,要么用useMemo稳定。 - useReducer 里写副作用:reducer 必须是纯函数,副作用放在
useEffect。 - 闭包陷阱:
useEffect、setTimeout、useCallback中读到的 state 是创建时的值。用函数式更新setX(prev => ...)或加依赖。 - Strict Mode 下 useEffect 执行两次:是有意为之,提示你检查清理函数是否完整。
- forwardRef 忘了:函数组件直接绑 ref 拿不到东西,
<MyComp ref={...}>会报警告。
13. 实战案例
详见 examples/ 目录:
custom-hook.jsx— 自定义useLocalStoragecontext-demo.jsx— Context + useReducer 实现轻量主题切换useReducer.jsx— 复杂状态机(购物车)memo-optimize.jsx— memo + useCallback 阻止子组件重渲染
demo/index.html — 主题切换 + 全局购物车(useContext + useReducer + useMemo 综合演练)
14. 一句话总结
React 进阶 = 用 Hooks 抽象逻辑(自定义 Hook),用缓存避免无效渲染(memo / useMemo / useCallback),用 Context 跨层传值,理解 Fiber 让你看懂调度的"为什么"。
15. 延伸阅读
- React 官方源码导览:https://github.com/acdlite/react-fiber-architecture
- React 18 并发模式:useTransition / useDeferredValue 实战
- 进阶库:React Query、Jotai、Recoil、Zustand