从基本概念到源码实现,全面解析 React Hooks
一、基本概念
1.1 什么是 Hooks?
Hooks 是 React 16.8 引入的新特性,它允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。
简单来说,Hooks 就是让你在函数组件中”钩入” React 特性的函数。
1.2 为什么需要 Hooks?
在 Hooks 出现之前,React 存在以下问题:
| 痛点 |
描述 |
| 状态逻辑难复用 |
使用 render props 和 HOC 会导致组件嵌套地狱 |
| 复杂组件难理解 |
生命周期中充斥大量无关逻辑,难以拆分 |
| Class 的学习成本 |
需要理解 this、绑定事件、生命周期等概念 |
| 逻辑分散 |
相关逻辑散落在 componentDidMount、componentDidUpdate、componentWillUnmount 中 |
1.3 核心 Hooks 一览
| Hook |
作用 |
useState |
在函数组件中添加 state |
useEffect |
执行副作用(订阅、请求、DOM 操作等) |
useContext |
读取 Context 值 |
useReducer |
useState 的替代方案,适合复杂 state 逻辑 |
useCallback |
缓存回调函数 |
useMemo |
缓存计算结果 |
useRef |
引用 DOM 或保存可变值 |
useLayoutEffect |
同步执行副作用,在 DOM 更新后、浏览器绘制前 |
二、常用 Hooks 详解与示例
2.1 useState
1 2 3 4 5 6 7 8 9 10 11 12 13
| function Counter() { const [count, setCount] = useState(0); const [user, setUser] = useState({ name: '', age: 0 });
return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>+1</button> {/* 函数式更新,适合依赖前一个 state */} <button onClick={() => setCount(prev => prev + 1)}>+1 (函数式)</button> </div> ); }
|
关键点:
useState 接收初始值,返回 [当前值, 更新函数]
- 初始值可以是函数(惰性初始化),用于避免每次渲染都执行 expensive 计算
1
| const [state, setState] = useState(() => computeExpensiveInitialState());
|
2.2 useEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function DataFetcher({ id }) { const [data, setData] = useState(null);
useEffect(() => { const controller = new AbortController(); fetch(`/api/users/${id}`, { signal: controller.signal }) .then(res => res.json()) .then(setData) .catch(console.error);
return () => controller.abort(); }, [id]);
return <div>{data ? JSON.stringify(data) : 'Loading...'}</div>; }
|
依赖数组规则:
- 不传:每次渲染后都执行
[]:仅挂载时执行一次
[a, b]:a 或 b 变化时执行
2.3 useRef
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function TextInput() { const inputRef = useRef(null);
const focusInput = () => { inputRef.current?.focus(); };
return ( <> <input ref={inputRef} /> <button onClick={focusInput}>聚焦</button> </> ); }
function Timer() { const countRef = useRef(0); countRef.current++; return <div>Render count: {countRef.current}</div>; }
|
2.4 useCallback 与 useMemo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function Parent() { const [count, setCount] = useState(0); const [text, setText] = useState('');
const handleClick = useCallback(() => { setCount(c => c + 1); }, []);
const expensiveValue = useMemo(() => { return computeExpensiveValue(count); }, [count]);
return <MemoChild onClick={handleClick} value={expensiveValue} />; }
|
三、Hooks 原理浅析
3.1 核心思想:链表存储
React 在内部用链表来存储 Hooks。每个函数组件对应一个 Fiber 节点,每个 Hook 是链表上的一个节点。
1 2 3 4 5
| Fiber 节点 └── memoizedState (第一个 Hook) ├── next -> (第二个 Hook) │ ├── next -> (第三个 Hook) │ │ └── ...
|
为什么是链表?
- 函数组件没有
this,无法像 Class 那样用实例存储
- 链表可以按调用顺序保存多个 Hook
- 每次渲染时按顺序遍历链表,对应到正确的 state
3.2 两个重要的链表
React 内部维护两套 Hook 链表:
- current:当前屏幕上显示的 UI 对应的 Hooks
- workInProgress:正在构建的新的 Hooks
渲染流程大致为:
1 2 3
| mount -> workInProgress 构建新链表 update -> 从 current 复制到 workInProgress,按顺序更新 commit -> workInProgress 替换 current
|
3.3 为什么 Hooks 必须在顶层调用?
1 2 3 4 5 6 7
| function Bad() { if (condition) { const [a, setA] = useState(0); } const [b, setB] = useState(0); }
|
React 依赖 Hooks 的调用顺序 来将 state 与正确的 Hook 关联。如果顺序变化,会错位导致 bug。
四、源码解读
4.1 简化的 Hook 数据结构
1 2 3 4 5 6 7 8
| type Hook = { memoizedState: any, baseState: any, baseQueue: Update | null, queue: UpdateQueue, next: Hook | null, };
|
4.2 useState 实现思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| let hooks = []; let currentHook = 0;
function useState(initialState) { const index = currentHook; hooks[index] = hooks[index] ?? (typeof initialState === 'function' ? initialState() : initialState);
function setState(newState) { hooks[index] = typeof newState === 'function' ? newState(hooks[index]) : newState; scheduleReRender(); }
currentHook++; return [hooks[index], setState]; }
function useMyComponent() { currentHook = 0; const [count, setCount] = useState(0); const [name, setName] = useState(''); }
|
4.3 useEffect 实现思路
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function useEffect(callback, deps) { const index = currentHook; const prevDeps = hooks[index];
const hasChanged = !prevDeps || deps?.some((d, i) => d !== prevDeps[i]);
if (hasChanged) { if (typeof prevEffect?.cleanup === 'function') { prevEffect.cleanup(); } const cleanup = callback(); hooks[index] = { deps, cleanup }; } currentHook++; }
|
五、自定义 Hook
5.1 封装业务逻辑
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
| function useLocalStorage(key, initialValue) { const [storedValue, setStoredValue] = useState(() => { try { const item = window.localStorage.getItem(key); return item ? JSON.parse(item) : initialValue; } catch (e) { return initialValue; } });
const setValue = useCallback((value) => { setStoredValue(prev => { const next = typeof value === 'function' ? value(prev) : value; window.localStorage.setItem(key, JSON.stringify(next)); return next; }); }, [key]);
return [storedValue, setValue]; }
function Settings() { const [theme, setTheme] = useLocalStorage('theme', 'light'); return <button onClick={() => setTheme(t => t === 'light' ? 'dark' : 'light')}>{theme}</button>; }
|
5.2 useDebounce
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function useDebounce(value, delay) { const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => { const timer = setTimeout(() => setDebouncedValue(value), delay); return () => clearTimeout(timer); }, [value, delay]);
return debouncedValue; }
function SearchInput() { const [search, setSearch] = useState(''); const debouncedSearch = useDebounce(search, 300);
useEffect(() => { if (!debouncedSearch) return; fetchResults(debouncedSearch); }, [debouncedSearch]); }
|
5.3 useFetch(数据请求)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function useFetch(url) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null);
useEffect(() => { setLoading(true); const controller = new AbortController();
fetch(url, { signal: controller.signal }) .then(res => res.json()) .then(setData) .catch(setError) .finally(() => setLoading(false));
return () => controller.abort(); }, [url]);
return { data, loading, error }; }
|
六、实际项目应用案例
6.1 场景一:表单管理(复杂表单)
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
| function useForm(initialValues) { const [values, setValues] = useState(initialValues); const [errors, setErrors] = useState({});
const handleChange = useCallback((name) => (e) => { setValues(prev => ({ ...prev, [name]: e.target.value })); }, []);
const validate = useCallback(() => { const newErrors = {}; if (!values.email) newErrors.email = '必填'; if (!values.password) newErrors.password = '必填'; setErrors(newErrors); return Object.keys(newErrors).length === 0; }, [values]);
const reset = useCallback(() => { setValues(initialValues); setErrors({}); }, []);
return { values, errors, handleChange, validate, reset }; }
function LoginForm() { const { values, errors, handleChange, validate } = useForm({ email: '', password: '' });
const onSubmit = (e) => { e.preventDefault(); if (!validate()) return; login(values); };
return ( <form onSubmit={onSubmit}> <input value={values.email} onChange={handleChange('email')} /> {errors.email && <span>{errors.email}</span>} <input type="password" value={values.password} onChange={handleChange('password')} /> {errors.password && <span>{errors.password}</span>} <button type="submit">登录</button> </form> ); }
|
6.2 场景二:无限滚动列表
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
| function useInfiniteScroll(callback, hasMore, loading) { const observerRef = useRef(); const lastElementRef = useCallback(node => { if (loading) return; if (observerRef.current) observerRef.current.disconnect(); observerRef.current = new IntersectionObserver(entries => { if (entries[0].isIntersecting && hasMore) { callback(); } }); if (node) observerRef.current.observe(node); }, [loading, hasMore, callback]);
return lastElementRef; }
function InfiniteList() { const [items, setItems] = useState([]); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const [loading, setLoading] = useState(false);
const loadMore = useCallback(async () => { setLoading(true); const res = await fetchItems(page); setItems(prev => [...prev, ...res.data]); setHasMore(res.hasMore); setPage(p => p + 1); setLoading(false); }, [page]);
const lastRef = useInfiniteScroll(loadMore, hasMore, loading);
return ( <ul> {items.map(item => <li key={item.id}>{item.name}</li>)} <li ref={lastRef} style={{ height: 1 }} /> </ul> ); }
|
6.3 场景三:权限与路由守卫
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 useAuth() { const [user, setUser] = useState(null); const [loading, setLoading] = useState(true);
useEffect(() => { authApi.getCurrentUser() .then(setUser) .finally(() => setLoading(false)); }, []);
const hasPermission = useCallback((permission) => { return user?.permissions?.includes(permission); }, [user]);
return { user, loading, hasPermission }; }
function ProtectedRoute({ permission, children }) { const { user, loading, hasPermission } = useAuth();
if (loading) return <Spin />; if (!user) return <Navigate to="/login" />; if (permission && !hasPermission(permission)) return <Forbidden />;
return children; }
|
6.4 场景四:性能优化(虚拟列表 + useMemo)
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
| function VirtualList({ items, itemHeight }) { const [scrollTop, setScrollTop] = useState(0); const containerRef = useRef();
const visibleRange = useMemo(() => { const start = Math.floor(scrollTop / itemHeight); const end = start + Math.ceil(containerRef.current?.clientHeight / itemHeight) + 2; return [Math.max(0, start), Math.min(items.length, end)]; }, [scrollTop, itemHeight, items.length]);
const visibleItems = useMemo(() => { const [start, end] = visibleRange; return items.slice(start, end).map((item, i) => ({ ...item, index: start + i, })); }, [items, visibleRange]);
return ( <div ref={containerRef} onScroll={e => setScrollTop(e.target.scrollTop)} style={{ height: '100%', overflow: 'auto' }} > <div style={{ height: items.length * itemHeight, position: 'relative' }}> {visibleItems.map(item => ( <div key={item.id} style={{ position: 'absolute', top: item.index * itemHeight }}> {item.content} </div> ))} </div> </div> ); }
|
七、常见陷阱与最佳实践
7.1 闭包陷阱
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function Bad() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(count + 1); }, 1000); return () => clearInterval(id); }, []); }
function Good() { const [count, setCount] = useState(0); useEffect(() => { const id = setInterval(() => { setCount(c => c + 1); }, 1000); return () => clearInterval(id); }, []); }
|
7.2 依赖数组的完整性
- 建议使用
eslint-plugin-react-hooks 的 exhaustive-deps 规则
- 不要随意省略依赖,可能导致 stale closure
7.3 何时用 useCallback / useMemo
- 需要传递回调给 memo 子组件时用
useCallback
- 计算成本高且依赖稳定时用
useMemo
- 不要过度使用,有时重新创建的开销小于记忆化
八、总结
| 主题 |
要点 |
| 概念 |
Hooks 让函数组件拥有 state 和副作用能力 |
| 原理 |
链表存储,依赖调用顺序,current/workInProgress 双缓冲 |
| 源码 |
极简实现可帮助理解,真实实现见 React 仓库 |
| 自定义 |
抽取可复用逻辑,命名以 use 开头 |
| 实践 |
避免闭包陷阱,合理使用依赖数组,按需优化 |
React Hooks 是现代 React 开发的核心,掌握其原理与最佳实践,能显著提升代码质量与开发效率。
参考资料