Frontend · #react#hooks#frontend

React Hooks深入解析与自定义Hook

2025.04.23 7 min 2.7k
// 目录 · contents

引言

React Hooks 自 16.8 版本引入以来,彻底改变了 React 组件的编写方式。Hooks 让函数组件拥有了状态管理、副作用处理等能力,同时也带来了全新的代码组织模式。本文将深入探讨 Hooks 的内部实现原理、使用规则背后的原因、自定义 Hook 的设计模式,以及利用 useMemo / useCallback 进行性能优化的最佳实践。

Hooks 的内部机制

Fiber 节点与 Hook 链表

React 在内部通过 Fiber 架构管理组件树。每个函数组件对应一个 Fiber 节点,而该节点上维护着一条 Hook 链表。每次调用一个 Hook(如 useState),React 都会在链表上创建或读取一个节点。

graph LR
    A[Fiber Node] --> B[memoizedState]
    B --> C[Hook 1: useState]
    C --> D[Hook 2: useEffect]
    D --> E[Hook 3: useMemo]
    E --> F[Hook 4: useCallback]
    style A fill:#61dafb,color:#000
    style C fill:#ffd700,color:#000
    style D fill:#98fb98,color:#000
    style E fill:#dda0dd,color:#000
    style F fill:#f0e68c,color:#000

这就是为什么 Hooks 必须在组件顶层调用——React 依赖调用顺序来匹配每个 Hook 与其对应的状态。

useState 内部实现简析

useState 在首次渲染(mount)和后续渲染(update)阶段有不同的行为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// Simplified internal implementation
function mountState(initialState) {
const hook = mountWorkInProgressHook();
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;

const queue = {
pending: null,
dispatch: null,
lastRenderedReducer: basicStateReducer,
lastRenderedState: initialState,
};
hook.queue = queue;

const dispatch = (queue.dispatch = dispatchSetState.bind(
null,
currentlyRenderingFiber,
queue
));

return [hook.memoizedState, dispatch];
}

function updateState() {
return updateReducer(basicStateReducer);
}

核心要点: - useState 本质上是 useReducer 的语法糖 - 初始值函数只在 mount 阶段执行一次 - dispatch 函数通过 bind 绑定到当前 Fiber 和 queue,因此引用稳定

useEffect 的执行时机

sequenceDiagram
    participant R as React Render
    participant B as Browser Paint
    participant E as useEffect
    participant L as useLayoutEffect

    R->>R: Render Phase (计算 Virtual DOM)
    R->>B: Commit Phase (更新 DOM)
    B->>L: useLayoutEffect 同步执行
    L->>B: 浏览器绘制
    B->>E: useEffect 异步执行
1
2
3
4
5
6
7
8
9
useEffect(() => {
// Side effect runs AFTER browser paint (asynchronous)
const subscription = api.subscribe(id);

return () => {
// Cleanup runs before next effect or unmount
subscription.unsubscribe();
};
}, [id]); // Dependency array controls when effect re-runs

关键区别: - useEffect:异步执行,不阻塞浏览器绘制 - useLayoutEffect:同步执行,在浏览器绘制前完成,适合 DOM 测量

useContext 的工作原理

useContext 订阅最近的 Provider 值。当 Provider 的 value 变化时,所有消费该 Context 的组件都会重新渲染:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const ThemeContext = React.createContext('light');

function App() {
const [theme, setTheme] = useState('light');

return (
<ThemeContext.Provider value={theme}>
<Header />
<Main />
<button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle Theme
</button>
</ThemeContext.Provider>
);
}

function Header() {
const theme = useContext(ThemeContext);
// Re-renders whenever theme changes
return <header className={`header-${theme}`}>My App</header>;
}

性能注意: Context 值变化会导致所有消费者组件重渲染,即使它们只用了部分数据。拆分 Context 或使用 useMemo 包裹 value 是常见的优化手段。

useReducer 的适用场景

当状态逻辑复杂、涉及多个子值、或下一状态依赖上一状态时,useReduceruseState 更适合:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const initialState = { count: 0, step: 1 };

function reducer(state, action) {
switch (action.type) {
case 'increment':
return { ...state, count: state.count + state.step };
case 'decrement':
return { ...state, count: state.count - state.step };
case 'setStep':
return { ...state, step: action.payload };
case 'reset':
return initialState;
default:
throw new Error(`Unknown action: ${action.type}`);
}
}

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

return (
<div>
<p>Count: {state.count} (step: {state.step})</p>
<button onClick={() => dispatch({ type: 'increment' })}>+</button>
<button onClick={() => dispatch({ type: 'decrement' })}>-</button>
<input
type="number"
value={state.step}
onChange={e => dispatch({ type: 'setStep', payload: +e.target.value })}
/>
</div>
);
}

Hooks 使用规则与背后原因

两条铁律

  1. 只在函数组件或自定义 Hook 的最顶层调用 Hooks
  2. 不在循环、条件判断或嵌套函数中调用 Hooks
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// BAD: conditional Hook call breaks the linked list order
function MyComponent({ showExtra }) {
const [name, setName] = useState('');

if (showExtra) {
// This breaks on re-render when showExtra changes!
const [extra, setExtra] = useState('');
}

useEffect(() => { /* ... */ }, []);
}

// GOOD: always call all Hooks, use conditions inside
function MyComponent({ showExtra }) {
const [name, setName] = useState('');
const [extra, setExtra] = useState('');

useEffect(() => {
if (showExtra) {
// Conditional logic inside the Hook
}
}, [showExtra]);
}

这两条规则的根本原因在于 React 依赖 Hook 的调用顺序来维护状态。如果调用顺序在两次渲染之间发生变化,Hook 与状态的对应关系就会错乱。

自定义 Hook 模式

自定义 Hook 是复用有状态逻辑的核心机制。以 use 开头命名,内部可以调用其他 Hooks。

模式一:数据获取 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);

const optionsRef = useRef(options);
optionsRef.current = options;

useEffect(() => {
const controller = new AbortController();
let cancelled = false;

async function fetchData() {
setLoading(true);
setError(null);

try {
const response = await fetch(url, {
...optionsRef.current,
signal: controller.signal,
});

if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}

const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled && err.name !== 'AbortError') {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
}

fetchData();

return () => {
cancelled = true;
controller.abort();
};
}, [url]);

return { data, loading, error };
}

// Usage
function UserProfile({ userId }) {
const { data: user, loading, error } = useFetch(`/api/users/${userId}`);

if (loading) return <Spinner />;
if (error) return <ErrorMessage error={error} />;
return <Profile user={user} />;
}

模式二:LocalStorage 持久化 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(`Error reading localStorage key "${key}":`, error);
return initialValue;
}
});

const setValue = useCallback((value) => {
try {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
} catch (error) {
console.error(`Error setting localStorage key "${key}":`, error);
}
}, [key, storedValue]);

return [storedValue, setValue];
}

// Usage
function Settings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
return <ThemeToggle value={theme} onChange={setTheme} />;
}

模式三:防抖 Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);

useEffect(() => {
const timer = setTimeout(() => setDebouncedValue(value), delay);
return () => clearTimeout(timer);
}, [value, delay]);

return debouncedValue;
}

// Usage in search
function SearchInput() {
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data: results } = useFetch(
debouncedQuery ? `/api/search?q=${debouncedQuery}` : null
);

return (
<div>
<input value={query} onChange={e => setQuery(e.target.value)} />
<SearchResults results={results} />
</div>
);
}

性能优化:useMemo 与 useCallback

useMemo:缓存计算结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function ProductList({ products, filter }) {
// Expensive filtering/sorting only re-runs when dependencies change
const filteredProducts = useMemo(() => {
return products
.filter(p => p.category === filter.category)
.filter(p => p.price >= filter.minPrice && p.price <= filter.maxPrice)
.sort((a, b) => a.price - b.price);
}, [products, filter.category, filter.minPrice, filter.maxPrice]);

return (
<ul>
{filteredProducts.map(p => (
<ProductItem key={p.id} product={p} />
))}
</ul>
);
}

useCallback:稳定函数引用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function ParentComponent() {
const [count, setCount] = useState(0);
const [text, setText] = useState('');

// Without useCallback, handleClick creates a new reference every render,
// causing ExpensiveChild to re-render even when only `text` changes
const handleClick = useCallback(() => {
setCount(c => c + 1);
}, []); // No dependencies — uses updater function

return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<ExpensiveChild onClick={handleClick} count={count} />
</div>
);
}

const ExpensiveChild = React.memo(({ onClick, count }) => {
console.log('ExpensiveChild rendered');
return <button onClick={onClick}>Count: {count}</button>;
});

优化决策流程

flowchart TD
    A[组件渲染性能问题?] --> B{是否确认存在性能问题?}
    B -->|否| C[不要过早优化]
    B -->|是| D{问题类型?}
    D -->|计算量大| E[useMemo 缓存计算]
    D -->|子组件频繁重渲染| F{子组件用了 React.memo?}
    F -->|否| G[先加 React.memo]
    F -->|是| H[useCallback 稳定回调引用]
    D -->|Context 导致大范围重渲染| I[拆分 Context / useMemo value]
    D -->|列表渲染慢| J[虚拟列表 + key 优化]
    style C fill:#90EE90,color:#000
    style E fill:#FFD700,color:#000
    style H fill:#FFD700,color:#000
    style I fill:#DDA0DD,color:#000

React 19 中的新 Hooks

React 19 引入了几个值得关注的新 Hook:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// useActionState: form action with pending state
function LoginForm() {
const [state, formAction, isPending] = useActionState(
async (prevState, formData) => {
const result = await login(formData);
return result.error ? { error: result.error } : { success: true };
},
{ error: null }
);

return (
<form action={formAction}>
<input name="email" type="email" />
<input name="password" type="password" />
<button disabled={isPending}>
{isPending ? 'Logging in...' : 'Login'}
</button>
{state.error && <p className="error">{state.error}</p>}
</form>
);
}

// use(): read resources in render
function UserProfile({ userPromise }) {
const user = use(userPromise); // Suspends until resolved
return <h1>{user.name}</h1>;
}

// useOptimistic: optimistic UI updates
function TodoList({ todos, addTodo }) {
const [optimisticTodos, addOptimistic] = useOptimistic(
todos,
(state, newTodo) => [...state, { ...newTodo, pending: true }]
);

async function handleAdd(formData) {
const newTodo = { text: formData.get('text'), id: Date.now() };
addOptimistic(newTodo);
await addTodo(newTodo);
}

return (
<div>
<form action={handleAdd}>
<input name="text" />
<button>Add</button>
</form>
<ul>
{optimisticTodos.map(todo => (
<li key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>
{todo.text}
</li>
))}
</ul>
</div>
);
}

总结

React Hooks 的设计精妙地将状态逻辑与组件解耦,理解其内部链表机制是正确使用的基础。自定义 Hook 是 React 中最强大的抽象手段,它允许在不增加组件层级的前提下复用有状态逻辑。性能优化方面,牢记”先测量、再优化”的原则,useMemouseCallback 只在确认存在性能瓶颈时才使用。随着 React 19 的推出,新的 Hook 进一步简化了表单处理和乐观更新等常见场景。

作者 · authorzt
发布 · date2025-04-23
篇幅 · length2.7k 字 · 7 min
许可 · licenseCC BY-SA 4.0
$ echo "comments" · 评论