useCallback 详解一、useCallback 基础1.1 什么是 useCallbackuseCallback 是一个用于缓存函数引用的 Hook返回一个记忆化的回调函数。1.2 基本语法const memoizedCallback useCallback(() { doSomething(a, b); }, [a, b]);第一个参数要缓存的函数第二个参数依赖数组返回值记忆化的函数引用1.3 基础示例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; });二、何时使用 useCallback2.1 传递给 memo 化的子组件const TodoItem React.memo(({ todo, onToggle, onDelete }) { console.log(TodoItem ${todo.id} 渲染); return ( li input typecheckbox 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 [filter, setFilter] useState(all); // ✅ 缓存回调函数避免子组件不必要的重渲染 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 ( div select value{filter} onChange{(e) setFilter(e.target.value)} option valueall全部/option option valueactive未完成/option option valuecompleted已完成/option /select ul {todos.map(todo ( TodoItem key{todo.id} todo{todo} onToggle{handleToggle} onDelete{handleDelete} / ))} /ul /div ); }2.2 作为其他 Hook 的依赖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); }, []); // search 函数不会变化 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 ); }2.3 防止不必要的 Effect 执行function DataFetcher({ userId, onSuccess }) { const [data, setData] useState(null); // ✅ 缓存 onSuccess避免每次渲染都触发 effect const handleSuccess useCallback((result) { setData(result); onSuccess?.(result); }, [onSuccess]); useEffect(() { fetch(/api/users/${userId}) .then(res res.json()) .then(handleSuccess); }, [userId, handleSuccess]); return div{data?.name}/div; }三、useCallback vs 普通函数3.1 性能对比function Comparison() { const [count, setCount] useState(0); const [text, setText] useState(); // 普通函数每次渲染都创建新函数 const normalClick () { console.log(count); }; // useCallback只在依赖变化时创建新函数 const memoizedClick useCallback(() { console.log(count); }, [count]); console.log(普通函数引用变化:, normalClick); console.log(缓存函数引用:, memoizedClick); return ( div button onClick{normalClick}普通函数/button button onClick{memoizedClick}缓存函数/button button onClick{() setCount(count 1)}增加计数/button input value{text} onChange{(e) setText(e.target.value)} / /div ); }3.2 引用相等性测试function ReferenceTest() { const [count, setCount] useState(0); // 每次渲染都是新函数 const fn1 () {}; // 函数引用稳定 const fn2 useCallback(() {}, []); useEffect(() { console.log(fn1 变化); // 每次渲染都打印 }, [fn1]); useEffect(() { console.log(fn2 变化); // 只打印一次 }, [fn2]); return button onClick{() setCount(count 1)}点击: {count}/button; }四、常见使用模式4.1 带参数的函数function ItemList({ items }) { // 方式1在回调中传递参数 const handleClick useCallback((id) { console.log(点击了, id); }, []); // 方式2使用>4.2 表单处理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 typesubmit提交/button /form ); }4.3 防抖和节流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 ); }五、性能优化最佳实践5.1 不要过度使用// ❌ 不必要的 useCallback没有传递给 memo 组件 function Component() { const handleClick useCallback(() { console.log(click); }, []); return button onClick{handleClick}点击/button; } // ✅ 直接使用普通函数 function Component() { const handleClick () { console.log(click); }; return button onClick{handleClick}点击/button; }5.2 正确设置依赖function CorrectDeps({ userId, onUpdate }) { // ✅ 包含所有依赖 const handleUpdate useCallback(() { onUpdate(userId); }, [userId, onUpdate]); // ✅ 使用函数式更新避免依赖 const handleIncrement useCallback(() { setCount(prev prev 1); // 不需要依赖 count }, []); }5.3 使用 useReducer 替代 useCallback// 复杂的状态更新逻辑可以使用 useReducer const reducer (state, action) { switch (action.type) { case increment: return { count: state.count 1 }; case decrement: return { count: state.count - 1 }; default: return state; } }; function Counter() { const [state, dispatch] useReducer(reducer, { count: 0 }); // dispatch 是稳定的不需要 useCallback return ( div p{state.count}/p button onClick{() dispatch({ type: increment })}/button button onClick{() dispatch({ type: decrement })}-/button /div ); }六、常见陷阱6.1 依赖数组错误function BadCallback() { const [count, setCount] useState(0); // ❌ 缺少 count 依赖函数内使用的是闭包中的旧值 const handleClick useCallback(() { console.log(count); // 永远打印 0 }, []); // ✅ 正确包含依赖 const handleClickCorrect useCallback(() { console.log(count); }, [count]); }6.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 ); }6.3 在循环中使用 useCallbackfunction 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 优化字段更新函数参考答案// 优化后的 Todo 列表 function TodoApp() { const [todos, setTodos] useState([]); const [input, setInput] useState(); const addTodo useCallback(() { if (input.trim()) { setTodos(prev [...prev, { id: Date.now(), text: input, completed: false }]); setInput(); } }, [input]); const toggleTodo useCallback((id) { setTodos(prev prev.map(todo todo.id id ? { ...todo, completed: !todo.completed } : todo ) ); }, []); const deleteTodo useCallback((id) { setTodos(prev prev.filter(todo todo.id ! id)); }, []); return ( div input value{input} onChange{(e) setInput(e.target.value)} / button onClick{addTodo}添加/button TodoList todos{todos} onToggle{toggleTodo} onDelete{deleteTodo} / /div ); } const TodoList React.memo(({ todos, onToggle, onDelete }) { return ( ul {todos.map(todo ( TodoItem key{todo.id} todo{todo} onToggle{onToggle} onDelete{onDelete} / ))} /ul ); });九、小结要点说明主要用途传递给 memo 子组件、作为其他 Hook 依赖不适用场景普通函数、未优化的子组件依赖数组必须正确声明所有依赖性能权衡useCallback 本身也有开销核心要点useCallback 用于缓存函数引用配合 React.memo 使用才有意义不要过度使用先测量再优化正确设置依赖数组