React Hooks

从基本概念到源码实现,全面解析 React Hooks


一、基本概念

1.1 什么是 Hooks?

Hooks 是 React 16.8 引入的新特性,它允许你在不编写 class 的情况下使用 state 以及其他的 React 特性。

简单来说,Hooks 就是让你在函数组件中”钩入” React 特性的函数

1.2 为什么需要 Hooks?

在 Hooks 出现之前,React 存在以下问题:

痛点 描述
状态逻辑难复用 使用 render propsHOC 会导致组件嵌套地狱
复杂组件难理解 生命周期中充斥大量无关逻辑,难以拆分
Class 的学习成本 需要理解 this、绑定事件、生命周期等概念
逻辑分散 相关逻辑散落在 componentDidMountcomponentDidUpdatecomponentWillUnmount

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);

// 清理函数:组件卸载或 effect 重新执行前调用
return () => controller.abort();
}, [id]); // 依赖数组: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 链表:

  1. current:当前屏幕上显示的 UI 对应的 Hooks
  2. 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
// React 内部简化版
type Hook = {
memoizedState: any, // 当前 state 值
baseState: any, // 基础 state
baseQueue: Update | null,
queue: UpdateQueue, // 更新队列
next: Hook | null, // 下一个 Hook(链表)
};

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
// 极简版 useState 实现(理解原理用)
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) {
// 先执行上一次的 cleanup
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
// useLocalStorage - 同步 state 到 localStorage
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
// ❌ 错误:setTimeout 中拿到的是旧 count
function Bad() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(count + 1); // count 永远是 0
}, 1000);
return () => clearInterval(id);
}, []); // 缺少 count 依赖
}

// ✅ 正确:使用函数式更新
function Good() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
}

7.2 依赖数组的完整性

  • 建议使用 eslint-plugin-react-hooksexhaustive-deps 规则
  • 不要随意省略依赖,可能导致 stale closure

7.3 何时用 useCallback / useMemo

  • 需要传递回调给 memo 子组件时用 useCallback
  • 计算成本高且依赖稳定时用 useMemo
  • 不要过度使用,有时重新创建的开销小于记忆化

八、总结

主题 要点
概念 Hooks 让函数组件拥有 state 和副作用能力
原理 链表存储,依赖调用顺序,current/workInProgress 双缓冲
源码 极简实现可帮助理解,真实实现见 React 仓库
自定义 抽取可复用逻辑,命名以 use 开头
实践 避免闭包陷阱,合理使用依赖数组,按需优化

React Hooks 是现代 React 开发的核心,掌握其原理与最佳实践,能显著提升代码质量与开发效率。


参考资料

Author

Felix Tao

Posted on

2021-01-04

Updated on

2022-01-21

Licensed under