useMemo vs useCallback:核心区别与使用场景
📌 一、useMemo:缓存计算结果
核心用途
避免在每次渲染时重复执行开销大的计算。
典型使用场景
场景1:数据转换/过滤/排序
jsx
import { useMemo } from 'react'; function MeterList({ meters, filterType, searchText }) { // 不缓存:每次渲染都重新过滤 → 性能浪费 const visibleMeters = meters .filter(m => m.type === filterType) .filter(m => m.id.includes(searchText)); // ✅ 缓存:只在依赖变化时重新计算 const cachedMeters = useMemo(() => { console.log('执行复杂过滤...'); return meters .filter(m => m.type === filterType) .filter(m => m.id.includes(searchText)); }, [meters, filterType, searchText]); return ( <div> {cachedMeters.map(m => <MeterCard key={m.id} meter={m} />)} </div> ); }
场景2:复杂计算(统计分析)
jsx
function Dashboard({ readings }) { // ✅ 缓存统计结果 const statistics = useMemo(() => { // 假设这里有大量计算:总和、平均值、趋势分析... const total = readings.reduce((sum, r) => sum + r.value, 0); const avg = total / readings.length; const trend = calculateTrend(readings); // 复杂算法 return { total, avg, trend }; }, [readings]); return ( <div> <p>总用水量: {statistics.total}</p> <p>平均值: {statistics.avg}</p> </div> ); }场景3:避免子组件不必要的重渲染(与React.memo配合)
jsx
const Chart = React.memo(({ data }) => { // 只有当data引用变化时才重绘 return <ExpensiveChart data={data} />; }); function Dashboard({ rawData }) { // ❌ 每次渲染都创建新对象 → Chart总是重绘 const chartData = { labels: rawData.map(d => d.time), values: rawData.map(d => d.value) }; // ✅ 缓存对象引用 → 只有rawData变化时才创建新对象 const cachedChartData = useMemo(() => ({ labels: rawData.map(d => d.time), values: rawData.map(d => d.value) }), [rawData]); return <Chart data={cachedChartData} />; }
📌 二、useCallback:缓存函数引用
核心用途
保持函数引用稳定,避免因函数重新创建导致子组件不必要的重渲染。
典型使用场景
场景1:将回调传递给React.memo优化的子组件
jsx
import { useCallback, useState } from 'react'; // 子组件使用 React.memo const MeterButton = React.memo(({ onClick, label }) => { console.log('按钮渲染:', label); return <button onClick={onClick}>{label}</button>; }); function MeterPanel({ meterId }) { const [count, setCount] = useState(0); // ❌ 每次渲染都创建新函数 → MeterButton总是重绘 const handleClick = () => { console.log('点击了:', meterId); }; // ✅ 缓存函数引用 → 只有meterId变化时才创建新函数 const handleClickCached = useCallback(() => { console.log('点击了:', meterId); }, [meterId]); return ( <div> <p>计数: {count}</p> <MeterButton onClick={handleClickCached} label="点击我" /> <button onClick={() => setCount(c => c + 1)}>增加计数</button> </div> ); }
场景2:作为useEffect的依赖
jsx
function WaterMonitor({ stationId, onStatusChange }) { // ✅ 如果onStatusChange是稳定的,这个effect不会无限循环 useEffect(() => { const ws = new WebSocket(`ws://api/station/${stationId}`); ws.onmessage = (event) => { onStatusChange(JSON.parse(event.data)); }; return () => ws.close(); }, [stationId, onStatusChange]); // onStatusChange稳定,effect只执行一次 } function Parent() { const [status, setStatus] = useState(null); // ✅ 缓存回调,避免引起子组件的effect无限循环 const handleStatusChange = useCallback((data) => { setStatus(data); }, []); // 空依赖,永远不变 return <WaterMonitor stationId="123" onStatusChange={handleStatusChange} />; }场景3:自定义Hook中返回函数
jsx
function useMeterActions(meterId) { const [data, setData] = useState(null); // ✅ 返回稳定的函数引用 const refresh = useCallback(async () => { const response = await fetch(`/api/meters/${meterId}`); const newData = await response.json(); setData(newData); }, [meterId]); const update = useCallback(async (updates) => { await fetch(`/api/meters/${meterId}`, { method: 'POST', body: JSON.stringify(updates) }); await refresh(); // 刷新数据 }, [meterId, refresh]); return { data, refresh, update }; }🔍 三、何时使用?(决策指南)
优先使用useMemo的场景
计算开销大:遍历大数组、复杂数学计算、数据转换
创建对象/数组:作为props传递给
React.memo子组件派生状态:从props或state计算新值
优先使用useCallback的场景
传递给
React.memo子组件的回调函数作为自定义Hook的返回值(稳定API)
作为其他Hooks的依赖(如useEffect)
不需要优化的场景
jsx
// ✅ 简单事件处理,不需要缓存 function SimpleButton() { const handleClick = () => { console.log('点击了'); }; return <button onClick={handleClick}>点击</button>; } // ✅ JSX中直接使用箭头函数(如果子组件没有优化) <button onClick={() => doSomething(id)}>操作</button>🎯 四、实战对比:智慧水务场景
让我们通过一个完整的例子来理解两者的区别:
jsx
import { useState, useMemo, useCallback } from 'react'; // 子组件 - 显示抄表记录 const ReadingTable = React.memo(({ records, onSelect }) => { console.log('ReadingTable 渲染'); return ( <table> {records.map(r => ( <tr key={r.id} onClick={() => onSelect(r)}> <td>{r.id}</td> <td>{r.value}</td> </tr> ))} </table> ); }); function WaterDashboard() { const [readings, setReadings] = useState([ { id: 1, value: 28, time: '09:12' }, { id: 2, value: 42, time: '10:45' }, // ...更多数据 ]); const [filter, setFilter] = useState('all'); const [selectedId, setSelectedId] = useState(null); // 1️⃣ useMemo: 缓存过滤后的数据 const filteredReadings = useMemo(() => { console.log('重新过滤数据...'); if (filter === 'all') return readings; return readings.filter(r => r.value > 30); }, [readings, filter]); // 2️⃣ useCallback: 缓存点击处理函数 const handleSelect = useCallback((record) => { console.log('选中:', record.id); setSelectedId(record.id); // 可能还有其他复杂逻辑 }, []); // 空依赖,因为setSelectedId是稳定的 // 3️⃣ 对比:如果不缓存 // const handleSelect = (record) => { // setSelectedId(record.id); // }; // ❌ 每次渲染都是新函数 → ReadingTable总是重绘 return ( <div> <select onChange={(e) => setFilter(e.target.value)}> <option value="all">全部</option> <option value="high">大于30</option> </select> {/* ReadingTable 只会在 filteredReadings 变化时重绘 */} <ReadingTable records={filteredReadings} onSelect={handleSelect} /> {selectedId && <div>当前选中: {selectedId}</div>} </div> ); }⚠️ 五、常见陷阱
陷阱1:过度优化
jsx
// ❌ 不要这样做 - 简单计算不需要useMemo function add(a, b) { return a + b; } const result = useMemo(() => add(x, y), [x, y]); // 浪费! // ✅ 直接计算即可 const result = x + y;陷阱2:依赖遗漏
jsx
function Component({ items, threshold }) { // ❌ 遗漏了threshold依赖 const filtered = useMemo(() => { return items.filter(item => item.value > threshold); }, [items]); // 当threshold变化时不会更新! // ✅ 正确做法 const filtered = useMemo(() => { return items.filter(item => item.value > threshold); }, [items, threshold]); }陷阱3:函数内部的闭包问题
jsx
function Timer({ interval }) { const [count, setCount] = useState(0); // ❌ 闭包陷阱:callback中使用了过时的count const handleTick = useCallback(() => { setCount(count + 1); // count始终是初始值 }, []); // ✅ 正确:使用函数式更新 const handleTick = useCallback(() => { setCount(c => c + 1); }, []); useEffect(() => { const timer = setInterval(handleTick, interval); return () => clearInterval(timer); }, [interval, handleTick]); return <div>{count}</div>; }📝 六、记忆口诀
useMemo:记住值,避免重计算
useCallback:记住函数,避免重创建
两个都依赖数组,依赖变时才更新
配合
React.memo使用,性能优化才完整不要过度优化,先测量再优化
记住这个简单的选择标准:如果你要缓存的是计算结果用useMemo,如果是函数本身用useCallback。
