4.什么时候使用 useCallback 和 useMemo?如果滥用它们会带来什么后果? 副本 来源:https://my.feishu.cn/docx/MkNedvderoIkjvxPFJwcvPUrnKh 采集状态:已完成正文采集;已滚动到底部并按块及代码行号采集 深入理解 React Hooks:何时使用 useMemo 与 useCallback?​ ​ 在 React 开发中,我们经常听到关于性能优化的建议,而 useMemo 和 useCallback 正是其中最常被提及的两个 Hooks。它们的目标很明确:**通过缓存(memoization)来避免不必要的计算和渲染**。​ ​ 然而,“何时使用”以及“是否应该使用”却是一个值得深入探讨的话un。过早或不当的优化,不仅无法带来收益,反而会增加代码的复杂度和维护成本。​ ​ 一切的起点:React 的重渲染机制​ ​ 要理解这两个 Hooks 的价值,我们首先需要回顾 React 的一个核心行为:**当一个组件的 state 或 props 发生变化时,它会触发自身以及其所有子组件的重渲染(re-render)**。​ ​ 在重渲染过程中,组件函数内部的所有代码都会被重新执行。这意味着:​ ​ 1. 函数内部的变量(包括对象和函数)会被重新创建。​ 2. 即使这些变量的内容“看起来”没变,但它们在内存中的引用地址却是全新的。 我们来看一个简单的例子:​ ​ function ParentComponent() { const [count, setCount] = useState(0); // 每次 ParentComponent 重渲染,这个对象都会被重新创建 const config = { theme: 'dark' }; // 这个函数也会被重新创建 const handleClick = () => { console.log("Button clicked"); }; return (
); } // 使用 React.memo 包装,仅在 props 变化时才重渲染 const ChildComponent = React.memo(function Child({ config, onClick }) { console.log("ChildComponent is rendering..."); return ; }); ​ 在这个例子中,即使我们用 React.memo 优化了 ChildComponent,但每次点击父组件的 Increment 按钮时,你仍然会在控制台看到 "ChildComponent is rendering..."。​ ​ 原因在于,父组件每次重渲染,都会生成全新的 config 对象和 handleClick 函数。对于 ChildComponent 而言,它接收到的 props 引用地址每次都不同,React.memo 的浅比较判断它们“发生了变化”,从而导致了不必要的重渲染。​ ​ 这就是 useMemo 和 useCallback 需要解决的核心问题之一:**保持引用的稳定性**。​ ​ useMemo:缓存计算结果​ ​ useMemo 用于缓存一个**计算结果(值)**。它会执行一个函数,并将其返回值缓存起来。只有当依赖项数组中的某个值发生变化时,它才会重新执行该函数。​ ​ 语法:​ const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);​ ​ 什么时候应该使用 useMemo?​ ​ 1. 进行昂贵的计算 这是 useMemo 最直接的应用场景。如果你的组件在每次渲染时都需要执行一个复杂的、耗时的计算(例如,对一个巨大的数组进行过滤、排序或转换),useMemo 可以显著提升性能。 function TodoList({ todos, filter }) { // 这个计算可能非常耗时,特别是当 todos 数组很大时 const visibleTodos = useMemo(() => { console.log("Filtering todos..."); return todos.filter(todo => todo.text.includes(filter)); }, [todos, filter]); // 仅在 todos 或 filter 变化时才重新计算 return ( ); } 在这个例子中,只要 todos 和 filter 不变,visibleTodos 就会直接从缓存中读取,避免了重复的 filter 操作。​ ​ 2. 向子组件传递对象或数组,以保证引用稳定 回到我们最初的例子,useMemo 可以用来解决 config 对象被频繁创建的问题。 function ParentComponent() { const [count, setCount] = useState(0); // config 对象现在被缓存了,它的引用不会在重渲染时改变 const config = useMemo(() => ({ theme: 'dark' }), []); // 空依赖数组,表示只在首次渲染时创建 // ... return (
{/* ... */}
); } 通过这种方式,ChildComponent 接收到的 config prop 将始终是同一个引用(除非 useMemo 的依赖项变化),React.memo 就能正确地跳过不必要的渲染。​ ​ useCallback:缓存函数本身​ ​ useCallback 与 useMemo 非常相似,但它专门用于缓存一个**函数本身**。​ ​ 语法:​ const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);​ ​ 实际上,useCallback(fn, deps) 等价于 useMemo(() => fn, deps)。它只是一个语法糖,让缓存函数的意图更明确。​ ​ 什么时候应该使用 useCallback?​ ​ 1. 向子组件传递函数,以保证引用稳定 这是 useCallback 最常见的用途。同样,在我们的第一个例子中,我们可以用它来稳定 handleClick 函数的引用。 function ParentComponent() { const [count, setCount] = useState(0); // handleClick 函数被缓存,引用保持稳定 const handleClick = useCallback(() => { console.log("Button clicked"); }, []); // 空依赖数组,函数永不重新创建 // ... return (
{/* ... */}
); } 这样,被 React.memo 包装的 ChildComponent 在父组件重渲染时,就不会因为 onClick prop 的变化而重渲染了。​ ​ 2. 作为其他 Hooks(如 useEffect)的依赖项 当你在 useEffect 的依赖数组中使用了一个函数时,如果这个函数没有被 useCallback 包装,它会在每次组件渲染时都变成一个新的函数,从而导致 useEffect 在每次渲染后都被触发。 function MyComponent({ userId }) { const [data, setData] = useState(null); // 如果 fetchData 没有被 useCallback 包装 // 每次渲染它都是新函数,会导致 effect 无限循环 const fetchData = useCallback(async () => { const response = await fetch(`/api/users/${userId}`); const result = await response.json(); setData(result); }, [userId]); // 仅在 userId 变化时,才创建新的 fetchData 函数 useEffect(() => { fetchData(); }, [fetchData]); // 依赖于一个稳定的函数引用 // ... } ​ 滥用的后果:为何不应“无脑”使用?​ ​ 既然这两个 Hooks 如此有用,为什么我们不把所有函数和对象都用它们包起来呢?因为**性能优化并非没有成本**。​ ​ 1. 增加了内存开销:useMemo 和 useCallback 本质上是在做缓存。缓存就需要占用内存来存储结果。如果对一些简单的、计算成本极低的值或函数进行缓存,占用的内存可能比节省的渲染成本还要高。 2. 增加了初始渲染的计算量:在组件首次渲染时,useMemo 和 useCallback 内部的逻辑本身也需要执行。此外,在每次重渲染时,React 还需要比较依赖项数组是否发生了变化。如果你的计算本身非常快,那么这些额外的比较和函数调用开销,可能会让你的组件变得更慢。 3. 增加了代码复杂度和心智负担: ◦ 代码可读性降低:满屏的 useMemo 和 useCallback 会让组件逻辑变得支离破碎。 ◦ 依赖项陷阱:忘记在依赖数组中包含所有相关的变量,是导致 bug 的常见原因(例如,函数中用到的 state 没有写进依赖,导致函数拿到的是陈旧的“闭包”值)。这要求开发者在编码时更加小心。 总结与实践原则​ ​ 那么,我们应该如何做出决策?以下是一些可以遵循的指导原则:​ ​ 1. 不要过早优化:这是最重要的原则。首先让你的代码能够正常工作,然后通过性能分析工具(如 React DevTools Profiler)去发现真正的性能瓶颈。 2. 定位问题再优化:只有当你通过分析工具,明确发现某个组件因为 props 引用变化而导致了不必要的、且代价高昂的重渲染时,才应该去考虑使用 useMemo 和 useCallback。 3. 明确使用场景: ◦ 使用 useMemo**: ▪ 当存在**真正昂贵**的计算时(如大数据量的数组操作)。 ▪ 当需要向一个被 React.memo 优化的子组件传递一个**对象或数组**时,为了保持引用稳定。 ◦ 使用 useCallback**: ▪ 当需要向一个被 React.memo 优化的子组件传递一个**函数**时。 ▪ 当一个函数需要被用作其他 Hooks(如 useEffect)的依赖项时。 4. 对于简单场景,保持简单:如果一个组件的重渲染开销很小,或者它没有复杂的子组件树,那么因为重渲染而重新创建一些函数和对象,完全是可以接受的。在这种情况下,强行优化只会徒增烦恼。 useMemo 和 useCallback 是 React 工具箱中强大的工具,但它们是“手术刀”,而非“万金油”。理解其背后的原理和成本,并在真正需要的地方审慎使用,才能让它们发挥出最大的价值。​ ​ 本文暂未被其它文档引用