useMemo 和 useCallback 的核心作用
这两个 Hooks 都是 React 提供的性能优化工具,它们的核心目标是减少不必要的计算和渲染。
-
useMemo:- 用途:记忆一个计算结果。它接收一个“创建”函数和一个依赖项数组。
useMemo会在初次渲染时执行该函数,并缓存其结果。在后续渲染中,只有当依赖项数组中的某个值发生变化时,它才会重新执行函数计算新值。否则,它将返回上一次缓存的值。 - 语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]); - 主要解决的问题:
- 避免在每次渲染时都进行昂贵的计算。
- 当一个对象或数组作为 props 传递给子组件时,如果这个对象/数组是在父组件渲染时即时创建的,那么即使它的内容没有变化,它的引用地址也会改变,导致依赖该 props 的
React.memo子组件或useEffect不必要地重新渲染/执行。useMemo可以确保在依赖不变的情况下返回相同的引用。
- 用途:记忆一个计算结果。它接收一个“创建”函数和一个依赖项数组。
-
useCallback:- 用途:记忆一个回调函数本身。它接收一个内联回调函数和一个依赖项数组。
useCallback会返回该回调函数的记忆版本,该回调函数仅在某个依赖项改变时才会更新。 - 语法:
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]); - 主要解决的问题:
- 与
useMemo类似,当一个函数作为 props 传递给子组件(尤其是用了React.memo的子组件)时,如果这个函数在父组件每次渲染时都重新创建,那么子组件会因为 props 变化而重新渲染。useCallback可以保证在依赖不变时返回相同的函数引用。 - 当一个函数作为依赖项传递给其他 Hooks (如
useEffect,useMemo, 甚至另一个useCallback) 时,如果该函数没有被useCallback包裹,它会在每次渲染时都是一个新的引用,可能导致依赖它的 Hook 不必要地执行。
- 与
- 用途:记忆一个回调函数本身。它接收一个内联回调函数和一个依赖项数组。
何时合理使用 useMemo 和 useCallback
并非所有地方都需要用它们。滥用反而可能导致性能下降(因为 Hooks 本身也有开销,比如依赖比较)。
useMemo 的合理使用场景:
-
昂贵的计算:
- 当你的组件中有一个计算量非常大(例如,对大型数组进行复杂转换、过滤、排序,或者递归计算)的函数,并且其结果在多次渲染之间可能保持不变时。
// 假设 filterAndSortLargeList 是一个非常耗时的操作 function MyComponent({ list, filterTerm, sortKey }) { const processedList = useMemo(() => { console.log("Performing expensive list processing..."); // 模拟耗时操作 let result = [...list]; if (filterTerm) { result = result.filter(item => item.name.includes(filterTerm)); } if (sortKey) { result.sort((a, b) => (a[sortKey] > b[sortKey] ? 1 : -1)); } // 可能还有更多复杂的转换 for (let i = 0; i < 1000000; i++) { /* no-op for demo */ } return result; }, [list, filterTerm, sortKey]); // 依赖项:当它们变化时才重新计算 return ( <ul> {processedList.map(item => <li key={item.id}>{item.name}</li>)} </ul> ); } -
确保传递给子组件的对象/数组的引用稳定性:
- 如果你将一个在渲染过程中动态创建的对象或数组作为 prop 传递给一个用
React.memo包裹的子组件,那么即使该对象/数组的内容没变,子组件也会因为 prop 的引用变化而重新渲染。
import React, { useState, useMemo, memo } from 'react'; const ChildComponent = memo(({ styleObject, dataArray }) => { console.log("ChildComponent re-rendered"); return ( <div style={styleObject}> Data length: {dataArray.length} {/* 假设这里有更复杂的渲染逻辑依赖 styleObject 和 dataArray */} </div> ); }); ChildComponent.displayName = "ChildComponent"; function ParentComponent() { const [theme, setTheme] = useState('light'); const [count, setCount] = useState(0); // 这个 state 的变化不应该影响 styleObject // 如果不使用 useMemo,styleObject 每次都会是一个新的对象引用 // const styleObject = { // backgroundColor: theme === 'light' ? '#fff' : '#333', // color: theme === 'light' ? '#000' : '#fff', // padding: 10 // }; // 使用 useMemo 确保 styleObject 的引用稳定性 const styleObject = useMemo(() => { console.log("Recalculating styleObject"); return { backgroundColor: theme === 'light' ? '#fff' : '#333', color: theme === 'light' ? '#000' : '#fff', padding: 10 }; }, [theme]); // 只有 theme 变化时才重新创建 styleObject // 假设 dataArray 是固定的,或者它的生成逻辑不应该因为 count 变化而重新执行 const dataArray = useMemo(() => { console.log("Recalculating dataArray"); return [{ id: 1, value: 'A' }, { id: 2, value: 'B' }]; }, []); // 空依赖数组,只创建一次 return ( <div> <button onClick={() => setTheme(theme === 'light' ? 'dark' : 'light')}> Toggle Theme </button> <button onClick={() => setCount(c => c + 1)}> Increment Count (forces parent re-render): {count} </button> <ChildComponent styleObject={styleObject} dataArray={dataArray} /> </div> ); } // export default ParentComponent;在这个例子中,如果 ParentComponent 因为
count状态变化而重新渲染,但theme没有变,styleObject会因为useMemo而保持引用不变,从而ChildComponent不会因为styleObject这个 prop 的变化而重新渲染。 - 如果你将一个在渲染过程中动态创建的对象或数组作为 prop 传递给一个用
useCallback 的合理使用场景:
-
将回调函数传递给
React.memo子组件:- 与
useMemo类似,如果传递给memo子组件的回调函数在父组件每次渲染时都重新创建,那么子组件仍然会重新渲染。
import React, { useState, useCallback, memo } from 'react'; const MemoizedButton = memo(({ onClick, children }) => { console.log(`Button "${children}" re-rendered`); return <button onClick={onClick}>{children}</button>; }); MemoizedButton.displayName = "MemoizedButton"; function CounterApp() { const [countA, setCountA] = useState(0); const [countB, setCountB] = useState(0); // 这个 state 用于触发父组件渲染 // 如果不使用 useCallback: // const handleIncrementA = () => setCountA(countA + 1); // 每次 CounterApp 渲染,handleIncrementA 都是一个新的函数实例 // 使用 useCallback: const handleIncrementA = useCallback(() => { setCountA(prevCountA => prevCountA + 1); // 使用函数式更新,避免把 countA 加入依赖 console.log("handleIncrementA called_ (memoized version if countB changed)"); }, []); // 依赖项为空,函数实例永不改变 (除非组件卸载) const handleIncrementB = useCallback(() => { setCountB(prevCountB => prevCountB + 1); console.log("handleIncrementB called (memoized version)"); }, []); // 一个依赖 countA 的回调 const logCountA = useCallback(() => { console.log("Current Count A:", countA); }, [countA]); // 当 countA 变化时,这个回调会是新的实例 return ( <div> <p>Count A: {countA}</p> <p>Count B: {countB}</p> <MemoizedButton onClick={handleIncrementA}>Increment A</MemoizedButton> <MemoizedButton onClick={handleIncrementB}>Increment B (triggers parent re-render)</MemoizedButton> <MemoizedButton onClick={logCountA}>Log Count A</MemoizedButton> </div> ); } // export default CounterApp;当点击 "Increment B" 时,
CounterApp重新渲染。由于handleIncrementA被useCallback包裹且依赖项为空,它仍然是同一个函数引用,所以MemoizedButton"Increment A" 不会因为onClickprop 变化而重新渲染。而logCountA因为依赖countA,如果countA变化,logCountA会是新的函数,对应的按钮可能会重新渲染。 - 与
-
作为其他 Hooks (如
useEffect) 的依赖项:- 如果一个函数在
useEffect的依赖数组中,并且这个函数没有被useCallback记忆,那么每次组件渲染时,这个函数都是一个新的引用,会导致useEffect在每次渲染后都执行。
import React, { useState, useEffect, useCallback } from 'react'; function DataFetcher({ userId }) { const [data, setData] = useState(null); const [otherState, setOtherState] = useState(0); // 用于触发组件重新渲染 // 错误的做法 (如果 fetchData 作为依赖): // const fetchData = () => { // console.log(`Fetching data for userId (unmemoized): ${userId}`); // fetch(`https://jsonplaceholder.typicode.com/todos/${userId}`) // .then(res => res.json()) // .then(json => setData(json)); // }; // useEffect(() => { // fetchData(); // }, [userId, fetchData]); // fetchData 每次都是新的,导致无限循环或不必要的请求 // 正确的做法 (使用 useCallback): const fetchData = useCallback(() => { console.log(`Fetching data for userId (memoized): ${userId}`); // 模拟API请求 const mockFetch = new Promise((resolve) => { setTimeout(() => { resolve({ id: userId, title: `Todo for user ${userId}` }); }, 500); }); mockFetch.then(json => setData(json)); }, [userId]); // 只有 userId 变化时,fetchData 才会是新的函数实例 useEffect(() => { console.log("useEffect triggered due to fetchData or userId change"); fetchData(); // 清理函数 (可选) return () => { console.log("Cleanup effect for userId:", userId); // 可以在这里中止 fetch 请求等 }; }, [fetchData]); // 现在 fetchData 是稳定的,effect 只在 fetchData (即 userId) 变化时运行 return ( <div> <button onClick={() => setOtherState(s => s + 1)}> Force Rerender (Other State: {otherState}) </button> {data ? <pre>{JSON.stringify(data, null, 2)}</pre> : <p>Loading...</p>} </div> ); } // export default function App() { // const [currentUserId, setCurrentUserId] = useState(1); // return ( // <> // <button onClick={() => setCurrentUserId(id => id + 1)}>Next User</button> // <DataFetcher userId={currentUserId} /> // </> // ); // }在这个例子中,如果
setOtherState导致DataFetcher重新渲染,但userId没变,那么fetchData函数因为useCallback仍然是同一个引用,所以useEffect不会重新执行。如果userId改变,fetchData会变成新的函数实例,useEffect正常执行。 - 如果一个函数在
经验法则与注意事项:
- 不要过早优化,不要过度优化:只有当性能分析(如 React DevTools Profiler)表明某个组件或计算确实是瓶颈时,才考虑使用它们。
- 依赖项数组至关重要:
- 正确性:必须包含函数/计算所依赖的所有外部变量。如果遗漏,可能会导致闭包捕获到过时的值,产生 bug。ESLint 的
eslint-plugin-react-hooks插件的exhaustive-deps规则可以帮助检查。 - 稳定性:依赖项数组本身的内容如果频繁变化,那么
useMemo/useCallback就失去了意义。有时需要进一步思考如何稳定依赖项(比如将对象依赖拆分为原始值依赖,或者使用useReducer管理复杂状态逻辑以获得稳定的dispatch函数)。
- 正确性:必须包含函数/计算所依赖的所有外部变量。如果遗漏,可能会导致闭包捕获到过时的值,产生 bug。ESLint 的
useCallback(fn, deps)等价于useMemo(() => fn, deps):useCallback只是useMemo用来记忆函数时的一个语法糖。- 简单函数/值的开销:对于非常简单、计算开销极低的函数或值,使用
useMemo/useCallback的成本(比较依赖项、存储等)可能超过它带来的收益。 React.memo是前提:如果子组件没有用React.memo(或PureComponent),那么即使你用useMemo/useCallback优化了传递给它的 props,子组件仍然可能因为父组件的重新渲染而重新渲染(除非子组件内部有其他优化逻辑)。
React 编译器 ("Forget") 与未来
React 团队正在开发一个名为 "Forget" 的实验性编译器。这个编译器的目标是自动地对 React 组件和 Hooks 进行记忆化优化。
原理概述:
- 静态分析:编译器会在构建时分析你的 React 代码。
- 理解依赖关系和可变性:它能理解哪些值在渲染之间是稳定的,哪些是可能变化的,以及函数和计算依赖于哪些值。
- 自动插入记忆化:基于分析结果,编译器会自动地、安全地插入类似于
useMemo和useCallback的优化逻辑,或者采用更底层的优化手段,而无需开发者手动编写这些 Hooks。
如果 "Forget" 编译器成熟并广泛应用:
- 减少手动优化负担:开发者将不再需要花费大量精力思考何时何地使用
useMemo和useCallback,以及如何正确管理它们的依赖项数组。这将大大简化代码,减少出错的可能性。 - 默认高性能:React 应用的性能会得到更普遍的提升,因为优化将是自动的,而不是依赖开发者手动应用。
- 更直观的代码:开发者可以编写更接近“纯粹” JavaScript 逻辑的组件代码,可读性可能会更高。
什么时候会不再需要它们?
- 这不是一个确切的版本号。React "Forget" 编译器目前仍在实验阶段。它需要经过充分的测试和迭代,才能成为 React 的标准部分,并且能够覆盖绝大多数需要手动记忆化的场景。
- 逐渐过渡:即使编译器推出,也可能有一个过渡期。可能最初它能处理大部分常见情况,但对于某些非常复杂的边缘场景,开发者仍然可能需要手动干预。
- 理念转变:如果编译器足够智能和可靠,那么
useMemo和useCallback可能会变成开发者不常直接使用的“底层工具”,或者仅在编译器无法自动优化的极少数情况下才需要。
** (React 19 及之前) 的状况:**
React 19 及之前,"Forget" 编译器尚未正式发布并集成到 React 的核心中。因此,useMemo 和 useCallback 仍然是 React 性能优化中非常重要且需要手动使用的工具。理解它们的原理和适用场景对于编写高性能的 React 应用至关重要。
总结代码要点与结构 (以 memenuhi "详细代码讲解" 的精神):
下面的代码片段不是一个单一的巨型文件,而是对上述概念的精炼和组织,并辅以注释。
// ========================================================================
// 场景 1: useMemo 优化昂贵计算
// (已在上方 ParentComponent 中通过 processedList 示例给出)
// ========================================================================
// ========================================================================
// 场景 2: useMemo 优化传递给 React.memo 子组件的对象/数组 props
// (已在上方 ParentComponent 和 ChildComponent 示例中通过 styleObject 和 dataArray 给出)
// ========================================================================
// ========================================================================
// 场景 3: useCallback 优化传递给 React.memo 子组件的回调 props
// (已在上方 CounterApp 和 MemoizedButton 示例中通过 handleIncrementA 给出)
// ========================================================================
// ========================================================================
// 场景 4: useCallback 优化作为 useEffect 依赖项的函数
// (已在上方 DataFetcher 示例中通过 fetchData 给出)
// ========================================================================
// ========================================================================
// 场景 5: 过度使用或不当使用的警示 (概念性)
// ========================================================================
function UnnecessaryMemoization({ simpleValue, onSimpleClick }) {
// 假设 simpleValue 只是一个数字或字符串,它的计算非常简单
// 假设 onSimpleClick 是一个简单的 setter,或者其逻辑不依赖复杂闭包
// !! 不必要的 useMemo !!
// 如果 getDisplayValue 本身计算开销极小
const displayValue = useMemo(() => {
console.log("Calculating displayValue (potentially unneeded memo)");
// 假设这里只是: return `Value: ${simpleValue}`;
return `Value: ${simpleValue}`; // 计算非常快
}, [simpleValue]);
// !! 可能不必要的 useCallback !!
// 如果 MyListItem 不是 memoized,或者 onSimpleClick 的创建开销极小
// 并且它不作为其他 hooks (如 useEffect) 的依赖
const handleClick = useCallback(() => {
console.log("handleClick called (potentially unneeded memo)");
onSimpleClick(simpleValue + 1);
}, [onSimpleClick, simpleValue]); // 这里的依赖项也需要小心
// 如果 MyListItem 是这样的:
// const MyListItem = ({ text, onClick }) => <li onClick={onClick}>{text}</li>;
// 那么 handleClick 的 memoization 就意义不大了,除非 MyListItem 被 React.memo 包裹
return <div onClick={handleClick}>{displayValue}</div>;
}
// ========================================================================
// 对依赖项的说明
// ========================================================================
function DependencyExample({ userSettings }) { // userSettings 是一个对象 { theme: 'dark', fontSize: 12 }
const [internalCount, setInternalCount] = useState(0);
// 假设我们有一个基于 userSettings.theme 的回调
// 错误: 依赖整个 userSettings 对象。如果 userSettings 的任何属性改变 (即使 theme 没变),
// 或者如果 userSettings 是父组件每次渲染都重新创建的对象,这个 callback 都会变。
// const handleThemeAction = useCallback(() => {
// console.log("Theme action for:", userSettings.theme);
// }, [userSettings]);
// 更好: 只依赖实际用到的原始值
const handleThemeAction = useCallback(() => {
console.log("Theme action for:", userSettings.theme);
// 假设这里还用到了 internalCount
console.log("Internal count during theme action:", internalCount);
}, [userSettings.theme, internalCount]); // 依赖更精确
// 如果 handleThemeAction 不需要访问最新的 internalCount,可以将 internalCount 从依赖中移除
// 如果需要访问但不希望它成为依赖 (例如,只是读取一下而不触发函数再生):
// 1. 使用 ref: const countRef = useRef(internalCount); useEffect(() => { countRef.current = internalCount });
// 然后在回调中读取 countRef.current
// 2. 如果是 setState,使用函数式更新: setCount(c => c + 1)
useEffect(() => {
handleThemeAction();
}, [handleThemeAction]);
return (
<button onClick={() => setInternalCount(c => c + 1)}>
Increment internal count: {internalCount}
</button>
);
}
// ========================================================================
// 总结与未来展望 (文字部分已说明)
// - useMemo: memoizes values
// - useCallback: memoizes functions
// - Key use cases: expensive computations, referential stability for props to memoized children,
// stable dependencies for other hooks.
// - "Forget" Compiler: Aims to automate these memoizations.
// - Current status: Still essential tools for manual optimization.
// ========================================================================
// 这里的代码行数主要通过注释和多个示例场景的组合来充实,
// 核心是把概念和用法通过不同的代码片段讲清楚。
// 对于这两个 Hooks 的讲解,代码的质量和清晰度比单纯的行数更重要。
// 模拟一个父组件来使用上面的组件
function AppShowcase() {
const [appTheme, setAppTheme] = useState('light');
const [mainList, setMainList] = useState([{id: 1, name: "Apple"}, {id: 2, name: "Banana"}]);
const [filter, setFilter] = useState('');
const userSpecificSettings = useMemo(() => ({
theme: appTheme,
fontSize: appTheme === 'light' ? 14 : 16,
// ...其他属性
}), [appTheme]);
const handleSimpleClick = useCallback((value) => {
console.log("AppShowcase: Simple click received with value", value);
}, []);
return (
<div>
<h1>React Memoization Showcase</h1>
<button onClick={() => setAppTheme(t => t === 'light' ? 'dark' : 'light')}>
Toggle App Theme
</button>
<input
type="text"
placeholder="Filter list"
value={filter}
onChange={e => setFilter(e.target.value)}
/>
<button onClick={() => setMainList([...mainList, {id: Date.now(), name: "New Fruit"}])}>
Add Fruit
</button>
<h2>MyComponent (Expensive List Processing)</h2>
<MyComponent list={mainList} filterTerm={filter} sortKey="name" />
<h2>Parent/Child with Memoized Props</h2>
<ParentComponent />
<h2>CounterApp with Memoized Callbacks</h2>
<CounterApp />
<h2>DataFetcher with Memoized Effect Callback</h2>
<DataFetcher userId={1} /> {/* 可以再加一个按钮改变 userId 来观察 */}
<h2>UnnecessaryMemoization Example</h2>
<UnnecessaryMemoization simpleValue={42} onSimpleClick={handleSimpleClick} />
<h2>DependencyExample</h2>
<DependencyExample userSettings={userSpecificSettings} />
</div>
);
}
// export default AppShowcase;
参考react19如何自动useMemo和useCallback juejin.cn/post/749791…