494 lines
14 KiB
Markdown
494 lines
14 KiB
Markdown
|
|
---
|
|||
|
|
tags: [React, Hooks, useState, useEffect, useContext, useRef, Frontend]
|
|||
|
|
create time: 2026-04-29 22:04
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 核心 Hooks
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
Hook 是 React 16.8 引入的特性,让函数组件拥有 State、副作用管理、上下文消费等原本只有 Class 组件才具备的能力。
|
|||
|
|
|
|||
|
|
> [!question] 思考:在 Hook 出现之前,Class 组件有哪些痛点?
|
|||
|
|
|
|||
|
|
| 痛点 | 表现 | Hook 的解法 |
|
|||
|
|
|------|------|-------------|
|
|||
|
|
| **状态逻辑复用** | 用 HOC / Render Props 嵌套过深("回调地狱") | 自定义 Hook 直接抽离状态逻辑 |
|
|||
|
|
| **职责分散** | 同一逻辑被拆分到 `componentDidMount`、`componentDidUpdate`、`componentWillUnmount` | 同一 `useEffect` 内组合关联逻辑 + cleanup |
|
|||
|
|
| **this 指向混乱** | 需要手动 bind、箭头函数或类字段属性 | 函数组件没有 `this`,闭包天然解决 |
|
|||
|
|
| **组件臃肿** | 大组件难以阅读和维护 | 按关注点拆分为多个 Hook |
|
|||
|
|
|
|||
|
|
Hook 的设计哲学可以总结为一句话:**把"组件做什么"和"组件什么时候做"解耦**。
|
|||
|
|
|
|||
|
|
本文聚焦四个核心 Hook —— **useState、useEffect、useContext、useRef**,并在此基础上介绍性能优化 Hook 与自定义 Hook 实战。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Hook 执行规则
|
|||
|
|
|
|||
|
|
> [!warning] 两条铁律
|
|||
|
|
> 1. **只在最顶层调用 Hook** — 不能在循环、条件、嵌套函数中调用
|
|||
|
|
> 2. **只在 React 函数组件或自定义 Hook 中调用** — 不能在其他普通 JS 函数中调用
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// ❌ 违反规则:条件调用 Hook
|
|||
|
|
function Component({ loaded }) {
|
|||
|
|
if (loaded) {
|
|||
|
|
const [data, setData] = useState(null); // 崩溃!
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ✅ 正确:条件逻辑放在 Hook 内部
|
|||
|
|
function Component({ loaded }) {
|
|||
|
|
const [data, setData] = useState(null);
|
|||
|
|
useEffect(() => {
|
|||
|
|
if (!loaded) return;
|
|||
|
|
fetch("/api/data").then(setData);
|
|||
|
|
}, [loaded]);
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Hook 的执行顺序依赖调用顺序
|
|||
|
|
|
|||
|
|
React 内部用一个数组存储每个组件的 Hook 状态。每次渲染时,**按声明顺序依次取出对应 Hook 的状态**。这就是为什么必须在顶层、固定顺序调用。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function Counter() {
|
|||
|
|
const [count, setCount] = useState(0); // Hook #0
|
|||
|
|
const theme = useContext(ThemeContext); // Hook #1
|
|||
|
|
const btnRef = useRef<HTMLButtonElement>(); // Hook #2
|
|||
|
|
|
|||
|
|
// 每次渲染都按这个顺序取状态,不能颠倒或删除
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## useState
|
|||
|
|
|
|||
|
|
### 基础用法
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const [state, setState] = useState<T>(initialValue);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
当初始值计算开销较大时,使用惰性初始化避免重复计算:
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const [items, setItems] = useState(() => loadFromStorage()); // 仅首次渲染执行
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 闭包陷阱(Closure Trap)
|
|||
|
|
|
|||
|
|
> [!warning] 高频踩坑点:React 每次渲染都创建全新的作用域和变量副本
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const [count, setCount] = useState(0);
|
|||
|
|
setTimeout(() => setCount(count + 1), 1000); // ❌ count 始终为首次渲染时的快照值
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**为什么?** React 的 `setCount` 不是异步函数,而是 **闭包捕获了渲染时的变量快照** —— 第一次渲染后,`handleClick` 里的 `count` 就被锁死在 `0` 上了。
|
|||
|
|
|
|||
|
|
> [!tip] 如何修复?
|
|||
|
|
> 有两种思路:让 React 自己提供最新状态(updater),或者用 ref 持有一个始终更新的引用。具体方案与对比见 → [[05-核心 Hooks/01-闭包陷阱|详解]]
|
|||
|
|
|
|||
|
|
### 关键行为
|
|||
|
|
|
|||
|
|
> [!tip] 核心认知
|
|||
|
|
> setState 是**异步合并**的 —— 你传给它的不是当前 state,而是基于"即将更新"的旧值计算新值。
|
|||
|
|
|
|||
|
|
| 场景 | 行为 |
|
|||
|
|
|------|------|
|
|||
|
|
| 对象/数组作为 state | **引用比较**——必须创建新对象/数组才会触发重渲染 |
|
|||
|
|
| 连续多次 setState | React 18 自动合并为一次 re-render |
|
|||
|
|
| 在非 React 代码中调用 | 使用 `startTransition` / `flushSync` 控制 |
|
|||
|
|
|
|||
|
|
### 不可变更新模式
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const [list, setList] = useState([{ id: 1, done: false }]);
|
|||
|
|
|
|||
|
|
// ❌ 直接修改 —— 不会触发重渲染
|
|||
|
|
list[0].done = true;
|
|||
|
|
setList(list);
|
|||
|
|
|
|||
|
|
// ✅ 创建新引用 —— 正确触发更新
|
|||
|
|
setList(prev => prev.map(item => item.id === 1 ? { ...item, done: true } : item));
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!note] 为什么要有不可变性?
|
|||
|
|
> React 通过**浅比较引用**来判断是否需要重渲染。如果原地修改了原有对象,引用地址不变,React 认为"没变化"就跳过了 DOM 更新。这正是 React 高效的原因之一。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## useEffect
|
|||
|
|
|
|||
|
|
### 基本模型
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
useEffect(() => {
|
|||
|
|
// 副作用逻辑
|
|||
|
|
return () => { /* cleanup */ }; // 可选清理函数
|
|||
|
|
}, [dependencies]); // 依赖数组
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 三种行为模式
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 无依赖数组 → 每次渲染后执行(几乎不推荐)
|
|||
|
|
useEffect(() => { console.log("every render"); });
|
|||
|
|
|
|||
|
|
// 空数组 [] → 仅挂载时执行一次
|
|||
|
|
useEffect(() => {
|
|||
|
|
console.log("mount once");
|
|||
|
|
return () => console.log("unmount");
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 有依赖项 → 任一依赖变化时重新执行
|
|||
|
|
useEffect(() => {
|
|||
|
|
const sub = api.subscribe(userId);
|
|||
|
|
return () => sub.unsubscribe();
|
|||
|
|
}, [userId]); // userId 变了 → 先 clean 旧的 → 再执行新的
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### useEffect 生命周期时序
|
|||
|
|
|
|||
|
|
> [!abstract] useEffect 的生命周期可以用下面的时序图理解:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
participant R as React
|
|||
|
|
participant E as Effect
|
|||
|
|
participant C as Cleanup
|
|||
|
|
|
|||
|
|
R->>E: 渲染完成,执行 effect
|
|||
|
|
Note over E: side effect 运行<br/>(数据请求、DOM 操作等)
|
|||
|
|
R->>R: 再次渲染...
|
|||
|
|
|
|||
|
|
alt 依赖项变化
|
|||
|
|
R->>C: 先执行上一次 effect 的 cleanup
|
|||
|
|
R->>E: 执行新的 effect
|
|||
|
|
else 依赖项未变化
|
|||
|
|
R->>R: 跳过本次 effect
|
|||
|
|
end
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!question] 思考:cleanup 函数何时执行?
|
|||
|
|
> - 组件卸载时
|
|||
|
|
> - **下一次 effect 执行前**(前提是依赖项发生了变化)
|
|||
|
|
> 这意味着你可以用同一个 effect 同时处理"订阅 → 取消 → 重新订阅"整个流程。
|
|||
|
|
|
|||
|
|
### 常见副作用类型
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 数据获取
|
|||
|
|
useEffect(() => {
|
|||
|
|
let cancelled = false;
|
|||
|
|
fetch("/api/data")
|
|||
|
|
.then(res => res.json())
|
|||
|
|
.then(data => { if (!cancelled) setData(data); });
|
|||
|
|
|
|||
|
|
return () => { cancelled = true; };
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// DOM 事件监听
|
|||
|
|
useEffect(() => {
|
|||
|
|
const handleResize = () => setWidth(window.innerWidth);
|
|||
|
|
window.addEventListener("resize", handleResize);
|
|||
|
|
return () => window.removeEventListener("resize", handleResize);
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
// 定时轮询(用 ref 避免把 fetchData 写入依赖数组导致频繁清理)
|
|||
|
|
useEffect(() => {
|
|||
|
|
const fetchData = () => {/* polling logic */};
|
|||
|
|
let mounted = true;
|
|||
|
|
|
|||
|
|
const timer = setInterval(async () => {
|
|||
|
|
if (!mounted) return;
|
|||
|
|
try {
|
|||
|
|
const res = await fetch("/api/data");
|
|||
|
|
const json = await res.json();
|
|||
|
|
setData(json);
|
|||
|
|
} catch (err) {
|
|||
|
|
setError(err instanceof Error ? err : new Error(String(err)));
|
|||
|
|
}
|
|||
|
|
}, 5000);
|
|||
|
|
|
|||
|
|
return () => { mounted = false; clearInterval(timer); };
|
|||
|
|
}, []);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 副作用分类指南
|
|||
|
|
|
|||
|
|
> [!summary] 哪些该放 useEffect?哪些不该?
|
|||
|
|
|
|||
|
|
| 应该用 useEffect | 不应该用 useEffect |
|
|||
|
|
|------------------|---------------------|
|
|||
|
|
| 数据获取、订阅、定时器 | JSX 渲染中的同步逻辑 |
|
|||
|
|
| 操作 DOM(focus、测量) | 阻止表单提交、路由跳转 |
|
|||
|
|
| 第三方库集成 | **事件处理函数体内**(直接用 onClick 即可) |
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## useMemo & useCallback
|
|||
|
|
|
|||
|
|
这两个 Hook 都是**记忆化**工具,核心目标相同:**缓存计算结果,避免不必要的重新计算或子组件重渲染**。
|
|||
|
|
|
|||
|
|
### useMemo —— 缓存计算结果
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const sortedItems = useMemo(() => items.sort(comparator), [items]);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!important] useMemo 不等于"不用每次都算"
|
|||
|
|
> `useMemo` 本身也有开销(缓存对比),它只是给开发者一个"提示 React:结果可以被复用"。**只有在计算昂贵或在渲染期间产生高成本时才有意义。**
|
|||
|
|
|
|||
|
|
### useCallback —— 缓存函数引用
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const handleClick = useCallback(
|
|||
|
|
(id: string) => { deleteItem(id); },
|
|||
|
|
[deleteItem] // deleteItem 变化时返回新函数
|
|||
|
|
);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 两者关系与选择
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
quadrantChart
|
|||
|
|
title "useMemo vs useCallback 使用决策"
|
|||
|
|
x-axis "少用" --> "常用"
|
|||
|
|
y-axis "缓存计算" --> "缓存引用"
|
|||
|
|
"useCallback": [0.7, 0.9]
|
|||
|
|
"useMemo": [0.3, 0.9]
|
|||
|
|
"纯计算,传给子组件": [0.7, 0.5]
|
|||
|
|
"纯值,渲染中使用": [0.3, 0.5]
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| Hook | 缓存什么 | 典型场景 |
|
|||
|
|
|------|----------|----------|
|
|||
|
|
| `useMemo` | 计算后的**值** | 列表排序、过滤、复杂运算 |
|
|||
|
|
| `useCallback` | 函数**引用** | 传给 `React.memo` 包裹的子组件 |
|
|||
|
|
|
|||
|
|
### 三者联合:防止过度重渲染
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function Parent() {
|
|||
|
|
const [text, setText] = useState("");
|
|||
|
|
|
|||
|
|
// 缓存 handler 引用 —— 避免 Child 因 props.fn 变化而重渲染
|
|||
|
|
const handleSubmit = useCallback(
|
|||
|
|
(e: React.FormEvent) => { alert(text); },
|
|||
|
|
[text]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
// 缓存 computed 值 —— 避免每次渲染都重新 sort/filter
|
|||
|
|
const filteredUsers = useMemo(
|
|||
|
|
() => users.filter(u => u.name.includes(text)),
|
|||
|
|
[users, text]
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
return <Child data={filteredUsers} onSubmit={handleSubmit} />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!example] 配合 React.memo 的效果
|
|||
|
|
> ```tsx
|
|||
|
|
> // Child 仅在 props 引用变化时才重渲染
|
|||
|
|
> const Child = React.memo(({ data, onSubmit }) => (
|
|||
|
|
> <div>{/* ... */}</div>
|
|||
|
|
> ));
|
|||
|
|
> ```
|
|||
|
|
> 如果没有 `useCallback` 和 `useMemo`,即使 `data` 和 `onSubmit` 内容没变,每次父组件渲染都会生成新引用,导致 Child 无效重渲染。
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## useContext
|
|||
|
|
|
|||
|
|
### 基本概念
|
|||
|
|
|
|||
|
|
Context 解决的是"跨层级传参"问题,无需逐层透传 props(又称 prop drilling)。
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const ThemeContext = createContext<"light" | "dark">("light");
|
|||
|
|
|
|||
|
|
function App() {
|
|||
|
|
return (
|
|||
|
|
<ThemeContext.Provider value="dark">
|
|||
|
|
<Header /> {/* 不需要 Header 向 Button 传递 theme */}
|
|||
|
|
<Main />
|
|||
|
|
</ThemeContext.Provider>
|
|||
|
|
);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function Button() {
|
|||
|
|
const theme = useContext(ThemeContext); // 任意深度消费
|
|||
|
|
return <button className={theme}>Click</button>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 最佳实践:封装自定义 Hook
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
const ThemeContext = createContext<"light" | "dark">("light");
|
|||
|
|
|
|||
|
|
// ❌ 不要到处写 useContext(ThemeContext)
|
|||
|
|
// ✅ 抽取为语义化 Hook
|
|||
|
|
function useTheme() {
|
|||
|
|
const ctx = useContext(ThemeContext);
|
|||
|
|
if (!ctx) throw new Error("useTheme must be used within a ThemeProvider");
|
|||
|
|
return ctx;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Context 的性能陷阱
|
|||
|
|
|
|||
|
|
> [!warning] Provider 的 value 一旦变化,**所有**消费该 Context 的组件都会重渲染
|
|||
|
|
>
|
|||
|
|
> 因为每次 `{ value: count }` 都是一个新对象,浅比较不等。
|
|||
|
|
|
|||
|
|
**解决方案:**
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 方案1:拆分成多个小 Context
|
|||
|
|
const UserContext = createContext(user);
|
|||
|
|
const ThemeContext = createContext(theme);
|
|||
|
|
|
|||
|
|
// 方案2:用 useReducer 保持 dispatch 引用稳定
|
|||
|
|
function App() {
|
|||
|
|
const [state, dispatch] = useReducer(reducer, initial);
|
|||
|
|
return <MyContext.Provider value={{ state, dispatch }}>
|
|||
|
|
<Children />
|
|||
|
|
</MyContext.Provider>;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!tip] 何时用 Context?
|
|||
|
|
> - 适合"全局配置":主题、语言、用户信息
|
|||
|
|
> - 不适合高频更新的值(频繁变化会导致大量重渲染)
|
|||
|
|
> - 复杂状态管理考虑 Zustand / Jotai / Redux Toolkit
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## useRef
|
|||
|
|
|
|||
|
|
### 两个用途
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
// 用途1:持有可变值,修改不触发重渲染
|
|||
|
|
const timerRef = useRef<number | null>(null);
|
|||
|
|
const prevCountRef = useRef<string>("");
|
|||
|
|
|
|||
|
|
// 用途2:访问 DOM 元素
|
|||
|
|
function FocusInput() {
|
|||
|
|
const inputRef = useRef<HTMLInputElement>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => { inputRef.current?.focus(); }, []);
|
|||
|
|
|
|||
|
|
return <input ref={inputRef} />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ref vs state 对比
|
|||
|
|
|
|||
|
|
| 维度 | state | ref |
|
|||
|
|
|------|-------|-----|
|
|||
|
|
| 修改触发重渲染 | ✅ | ❌ |
|
|||
|
|
| 跨渲染持久化 | ✅ | ✅ |
|
|||
|
|
| 写入方式 | `setState(val)` | `ref.current = val` |
|
|||
|
|
| 典型用途 | UI 驱动数据 | DOM 句柄 / 定时器 / 临时计数器 |
|
|||
|
|
|
|||
|
|
> [!note] 本质区别
|
|||
|
|
> ref 的 `.current` 是一个普通的 JavaScript 属性,修改它就像改任何对象的属性一样,React 完全不知道。State 是 React 管理的响应式数据,修改后会告诉 React "请重新渲染"。
|
|||
|
|
|
|||
|
|
### 实用技巧:保存上一次的值
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function usePrevious<T>(value: T): T | undefined {
|
|||
|
|
const ref = useRef<T>();
|
|||
|
|
useEffect(() => { ref.current = value; });
|
|||
|
|
return ref.current;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 用法
|
|||
|
|
function Example({ value }) {
|
|||
|
|
const prev = usePrevious(value);
|
|||
|
|
// prev !== undefined && prev !== value → 值变更了
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 自定义 Hook 实战
|
|||
|
|
|
|||
|
|
自定义 Hook 是 React 中最强大的抽象手段之一 —— 它是一个普通函数,名字以 `use` 开头,内部可以调用其他 Hook,从而实现**状态逻辑的跨组件复用**。
|
|||
|
|
|
|||
|
|
### 示例:自动失焦 Hook
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function useAutoFocus(delayMs: number = 300) {
|
|||
|
|
const ref = useRef<HTMLInputElement>(null);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
const timer = setTimeout(() => ref.current?.focus(), delayMs);
|
|||
|
|
return () => clearTimeout(timer);
|
|||
|
|
}, [delayMs]);
|
|||
|
|
|
|||
|
|
return ref; // 暴露给组件绑定到 input
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 使用
|
|||
|
|
function LoginForm() {
|
|||
|
|
const usernameRef = useAutoFocus();
|
|||
|
|
return <input ref={usernameRef} placeholder="Username" />;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 示例:在线状态检测
|
|||
|
|
|
|||
|
|
```tsx
|
|||
|
|
function useIsOnline() {
|
|||
|
|
const [online, setOnline] = useState(navigator.onLine);
|
|||
|
|
|
|||
|
|
useEffect(() => {
|
|||
|
|
setOnline(navigator.onLine);
|
|||
|
|
const onOnline = () => setOnline(true);
|
|||
|
|
const onOffline = () => setOnline(false);
|
|||
|
|
window.addEventListener("online", onOnline);
|
|||
|
|
window.addEventListener("offline", onOffline);
|
|||
|
|
return () => {
|
|||
|
|
window.removeEventListener("online", onOnline);
|
|||
|
|
window.removeEventListener("offline", onOffline);
|
|||
|
|
};
|
|||
|
|
}, []);
|
|||
|
|
|
|||
|
|
return online;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 自定义 Hook 设计原则
|
|||
|
|
|
|||
|
|
> [!summary] 好的自定义 Hook 应遵循以下原则
|
|||
|
|
> 1. **单一职责** —— 每个 Hook 解决一个问题(如上述两个例子各自独立)
|
|||
|
|
> 2. **命名清晰** —— `useXxx` 格式,读起来像动作:"监听网络"、"记住上次值"
|
|||
|
|
> 3. **返回值明确** —— 返回需要的引用或状态,不要暴露过多内部细节
|
|||
|
|
> 4. **可以组合** —— `useFetch` 内部可用 `useEffect` + `useRef`,外层可再用 `useAutoFocus`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## Hook 调试技巧
|
|||
|
|
|
|||
|
|
> [!tip] React DevTools 浏览器扩展
|
|||
|
|
> 安装后可以:
|
|||
|
|
> - 查看每个组件的 Hook 值和更新次数
|
|||
|
|
> - 定位"为什么这个组件在重渲染"
|
|||
|
|
> - 记录渲染时间,发现性能瓶颈
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[3. 组件篇/01-组件基础.md]]
|
|||
|
|
- [[3. 组件篇/03-性能优化.md]]
|
|||
|
|
- [[config/REACT/react-best-practices.md]]
|