03. useCallback - 函数缓存
一、5W1H 概述
| 维度 | 内容 |
|---|---|
| What | 缓存函数引用,避免函数在每次渲染时重新创建 |
| Why | 配合 React.memo 优化子组件渲染,作为 useEffect 依赖 |
| When | 函数传递给 memo 化的子组件、作为 useEffect 依赖 |
| Where | 函数组件顶层 |
| Who | 需要优化函数引用的开发者 |
| How | const memoizedCallback = useCallback(() => fn(), [deps]) |
二、What - 什么是 useCallback?
useCallback 是一个用于缓存函数引用的 Hook,返回一个记忆化的回调函数。
import { useCallback } from 'react'; const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);三、Why - 为什么需要 useCallback?
3.1 避免子组件不必要的重渲染
配合 React.memo 使用时,如果函数引用每次都变化,memo 优化会失效。
// ❌ 每次渲染都创建新函数,导致 Child 重渲染 function Parent() { const handleClick = () => console.log('click'); return <Child onClick={handleClick} />; } // ✅ 使用 useCallback 稳定函数引用 function Parent() { const handleClick = useCallback(() => console.log('click'), []); return <Child onClick={handleClick} />; }3.2 作为 useEffect 的依赖
当函数作为 useEffect 依赖时,需要稳定引用避免 effect 频繁执行。
四、When - 何时使用 useCallback?
| 场景 | 是否使用 | 说明 |
|---|---|---|
| 传递给 memo 化的子组件 | ✅ 推荐 | 避免子组件重渲染 |
| 作为 useEffect 依赖 | ✅ 推荐 | 避免 effect 频繁执行 |
| 普通函数(未传递) | ❌ 不推荐 | 优化成本大于收益 |
| 传递给未 memo 的组件 | ❌ 不推荐 | 没有优化效果 |
五、Where - 在哪里使用?
- 函数组件的顶层
- 自定义 Hook 中
// ✅ 正确:组件顶层 function MyComponent() { const handleClick = useCallback(() => {}, []); } // ✅ 正确:自定义 Hook function useCustomHandler() { const handler = useCallback(() => {}, []); return handler; }六、Who - 谁需要使用?
需要优化性能、避免不必要重渲染的 React 开发者。
七、How - 如何使用 useCallback?
7.1 基础示例
import { useCallback, useState } from 'react'; function Parent() { const [count, setCount] = useState(0); const [text, setText] = useState(''); // ❌ 每次渲染都创建新函数 const handleClick = () => { console.log('点击了', count); }; // ✅ 只在 count 变化时创建新函数 const memoizedHandleClick = useCallback(() => { console.log('点击了', count); }, [count]); return ( <div> <Child onClick={memoizedHandleClick} /> <button onClick={() => setCount(count + 1)}>增加计数</button> <input value={text} onChange={(e) => setText(e.target.value)} /> </div> ); } const Child = React.memo(({ onClick }) => { console.log('Child 渲染'); return <button onClick={onClick}>点击</button>; });7.2 传递给 memo 子组件
const TodoItem = React.memo(({ todo, onToggle, onDelete }) => { console.log(`TodoItem ${todo.id} 渲染`); return ( <li> <input type="checkbox" checked={todo.completed} onChange={() => onToggle(todo.id)} /> <span>{todo.text}</span> <button onClick={() => onDelete(todo.id)}>删除</button> </li> ); }); function TodoList() { const [todos, setTodos] = useState([ { id: 1, text: '学习 React', completed: false }, { id: 2, text: '学习 useCallback', completed: false } ]); // ✅ 缓存回调函数,避免子组件不必要的重渲染 const handleToggle = useCallback((id) => { setTodos(prev => prev.map(todo => todo.id === id ? { ...todo, completed: !todo.completed } : todo ) ); }, []); // 依赖为空,因为使用了函数式更新 const handleDelete = useCallback((id) => { setTodos(prev => prev.filter(todo => todo.id !== id)); }, []); return ( <ul> {todos.map(todo => ( <TodoItem key={todo.id} todo={todo} onToggle={handleToggle} onDelete={handleDelete} /> ))} </ul> ); }7.3 作为 useEffect 依赖
function SearchComponent() { const [query, setQuery] = useState(''); const [results, setResults] = useState([]); // ✅ 缓存函数,避免 useEffect 频繁执行 const search = useCallback(async (searchQuery) => { const response = await fetch(`/api/search?q=${searchQuery}`); const data = await response.json(); setResults(data); }, []); useEffect(() => { if (query) { search(query); } }, [query, search]); // search 稳定,不会导致无限循环 return ( <div> <input value={query} onChange={(e) => setQuery(e.target.value)} /> <ul>{results.map(item => <li key={item.id}>{item.name}</li>)}</ul> </div> ); }7.4 带参数的函数
function ItemList({ items }) { // 方式1:在回调中传递参数 const handleClick = useCallback((id) => { console.log('点击了', id); }, []); // 方式2:使用>7.5 表单处理function Form() { const [formData, setFormData] = useState({ name: '', email: '', message: '' }); // 通用的字段更新函数 const updateField = useCallback((field) => (e) => { setFormData(prev => ({ ...prev, [field]: e.target.value })); }, []); const handleSubmit = useCallback((e) => { e.preventDefault(); console.log('提交:', formData); }, [formData]); return ( <form onSubmit={handleSubmit}> <input value={formData.name} onChange={updateField('name')} placeholder="姓名" /> <input value={formData.email} onChange={updateField('email')} placeholder="邮箱" /> <textarea value={formData.message} onChange={updateField('message')} placeholder="消息" /> <button type="submit">提交</button> </form> ); }
7.6 防抖和节流
function DebouncedSearch() { const [searchTerm, setSearchTerm] = useState(''); const [results, setResults] = useState([]); // 实际的搜索函数 const performSearch = useCallback(async (query) => { const response = await fetch(`/api/search?q=${query}`); const data = await response.json(); setResults(data); }, []); // 防抖版本 const debouncedSearch = useCallback( debounce((query) => { if (query) performSearch(query); }, 500), [performSearch] ); const handleChange = useCallback((e) => { const value = e.target.value; setSearchTerm(value); debouncedSearch(value); }, [debouncedSearch]); return ( <div> <input value={searchTerm} onChange={handleChange} placeholder="搜索..." /> <ul>{results.map(item => <li key={item.id}>{item.name}</li>)}</ul> </div> ); }
八、常见陷阱
8.1 依赖数组错误
function BadCallback() { const [count, setCount] = useState(0); // ❌ 缺少 count 依赖,函数内使用的是闭包中的旧值 const handleClick = useCallback(() => { console.log(count); // 永远打印 0 }, []); // ✅ 正确包含依赖 const handleClickCorrect = useCallback(() => { console.log(count); }, [count]); }
8.2 与 React.memo 配合不当
const Child = React.memo(({ onClick, data }) => { // ... }); function Parent() { const [text, setText] = useState(''); // ❌ 每次渲染都创建新函数,导致 Child 重渲染 const handleClick = () => { console.log('click'); }; // ✅ 缓存函数,避免 Child 重渲染 const memoizedClick = useCallback(() => { console.log('click'); }, []); return ( <div> <Child onClick={memoizedClick} data={data} /> <input value={text} onChange={(e) => setText(e.target.value)} /> </div> ); }
8.3 在循环中使用 useCallback
function ItemList({ items }) { // ❌ 为每个 item 创建单独的 useCallback(不必要) const handlers = items.map(item => ({ onEdit: useCallback(() => editItem(item.id), []), onDelete: useCallback(() => deleteItem(item.id), []) })); // ✅ 使用单个回调,通过参数区分 const handleEdit = useCallback((id) => { editItem(id); }, []); const handleDelete = useCallback((id) => { deleteItem(id); }, []); return ( <ul> {items.map(item => ( <li key={item.id}> <button onClick={() => handleEdit(item.id)}>编辑</button> <button onClick={() => handleDelete(item.id)}>删除</button> </li> ))} </ul> ); }
九、useCallback vs useMemo
// useCallback 缓存函数 const fn = useCallback(() => { doSomething(a, b); }, [a, b]); // useMemo 缓存函数(等价写法) const fn = useMemo(() => { return () => doSomething(a, b); }, [a, b]); // 何时使用: // - useCallback: 缓存回调函数 // - useMemo: 缓存计算结果
十、练习题
基础题
- 使用 useCallback 优化传递给子组件的回调
- 实现一个 Todo 列表,使用 useCallback 优化添加、删除、切换操作
进阶题
- 实现一个搜索组件,使用 useCallback 配合防抖
- 实现一个表单,使用 useCallback 优化字段更新函数
十一、小结
要点 说明 主要用途 传递给 memo 子组件、作为其他 Hook 依赖 不适用场景 普通函数、未优化的子组件 依赖数组 必须正确声明所有依赖 性能权衡 useCallback 本身也有开销