03. useCallback - 函数缓存一、5W1H 概述维度内容What缓存函数引用避免函数在每次渲染时重新创建Why配合 React.memo 优化子组件渲染作为 useEffect 依赖When函数传递给 memo 化的子组件、作为 useEffect 依赖Where函数组件顶层Who需要优化函数引用的开发者Howconst memoizedCallback useCallback(() fn(), [deps])二、What - 什么是 useCallbackuseCallback 是一个用于缓存函数引用的 Hook返回一个记忆化的回调函数。import { useCallback } from react; const memoizedCallback useCallback(() { doSomething(a, b); }, [a, b]);三、Why - 为什么需要 useCallback3.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 - 如何使用 useCallback7.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 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 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 typesubmit提交/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 在循环中使用 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 优化字段更新函数十一、小结要点说明主要用途传递给 memo 子组件、作为其他 Hook 依赖不适用场景普通函数、未优化的子组件依赖数组必须正确声明所有依赖性能权衡useCallback 本身也有开销