文章目錄
- 什么是 useMemo?
- 基本語法
- 使用場景
- 實際例子
- 什么是 useCallback?
- 基本語法
- 使用場景
- 實際例子
- 核心區別對比
- 什么時候使用它們?
- 使用 useMemo 的時機
- 使用 useCallback 的時機
- 常見誤區和注意事項
- 誤區 1:過度使用
- 誤區 2:依賴數組不正確
- 誤區 3:在條件語句中使用
- 實際應用示例
在 React 開發中,性能優化始終是一個重要話題。隨著應用復雜度的增加,不必要的重新渲染和重復計算會嚴重影響用戶體驗。React 提供了兩個強大的 Hook —— useMemo
和 useCallback
來幫助解決這些問題。雖然它們都用于性能優化,但各有不同的使用場景和目的。
什么是 useMemo?
useMemo
是一個用于緩存計算結果的 Hook。它會在依賴項發生變化時才重新計算值,否則返回上次緩存的結果。
基本語法
const memoizedValue = useMemo(() => {// 執行昂貴的計算return expensiveCalculation(a, b);
}, [a, b]); // 依賴數組
使用場景
- 昂貴的計算操作:避免在每次渲染時重復執行復雜計算
- 避免創建新的對象或數組:防止子組件不必要的重新渲染
實際例子
import React, { useMemo, useState } from 'react';function ExpensiveComponent({ items, filter }) {const [count, setCount] = useState(0);// 沒有使用 useMemo - 每次渲染都會重新計算const expensiveValue = items.filter(item => item.category === filter).reduce((sum, item) => sum + item.price, 0);// 使用 useMemo - 只有當 items 或 filter 改變時才重新計算const memoizedValue = useMemo(() => {console.log('重新計算昂貴的值');return items.filter(item => item.category === filter).reduce((sum, item) => sum + item.price, 0);}, [items, filter]);return (<div><p>計算結果: {memoizedValue}</p><p>計數: {count}</p><button onClick={() => setCount(count + 1)}>點擊計數 (不會觸發重新計算)</button></div>);
}
什么是 useCallback?
useCallback
是一個用于緩存函數的 Hook。它返回一個記憶化的回調函數,只有在依賴項發生變化時才會返回新的函數。
基本語法
const memoizedCallback = useCallback(() => {// 函數邏輯doSomething(a, b);
}, [a, b]); // 依賴數組
使用場景
- 傳遞給子組件的回調函數:避免子組件因為新函數引用而重新渲染
- 作為其他 Hook 的依賴:確保依賴數組的穩定性
實際例子
import React, { useCallback, useState, memo } from 'react';// 子組件使用 memo 包裝,只有 props 改變時才重新渲染
const ChildComponent = memo(({ onClick, name }) => {console.log(`${name} 組件渲染了`);return <button onClick={onClick}>點擊 {name}</button>;
});function ParentComponent() {const [count, setCount] = useState(0);const [name, setName] = useState('');// 沒有使用 useCallback - 每次渲染都會創建新函數const handleClick1 = () => {console.log('點擊了按鈕1');};// 使用 useCallback - 函數引用保持穩定const handleClick2 = useCallback(() => {console.log('點擊了按鈕2');}, []); // 空依賴數組,函數永遠不會改變// 依賴于 count 的回調函數const handleClick3 = useCallback(() => {console.log(`當前計數: ${count}`);}, [count]); // 只有 count 改變時才創建新函數return (<div><p>計數: {count}</p><input value={name} onChange={(e) => setName(e.target.value)}placeholder="輸入名稱"/><button onClick={() => setCount(count + 1)}>增加計數</button>{/* 每次父組件重新渲染時,子組件也會重新渲染 */}<ChildComponent onClick={handleClick1} name="按鈕1(未優化)" />{/* 只有當 handleClick2 改變時,子組件才會重新渲染 */}<ChildComponent onClick={handleClick2} name="按鈕2(已優化)" />{/* 只有當 count 改變時,子組件才會重新渲染 */}<ChildComponent onClick={handleClick3} name="按鈕3(依賴count)" /></div>);
}
核心區別對比
特性 | useMemo | useCallback |
---|---|---|
緩存對象 | 緩存計算結果(值) | 緩存函數本身 |
返回值 | 返回計算后的值 | 返回記憶化的函數 |
主要用途 | 避免重復計算 | 避免函數重新創建 |
典型場景 | 復雜計算、數據處理 | 事件處理器、回調函數 |
性能影響 | 減少計算開銷 | 減少子組件重新渲染 |
什么時候使用它們?
使用 useMemo 的時機
- 計算開銷很大:復雜的數學運算、大數據處理
- 創建復雜對象:避免每次渲染都創建新的對象或數組
- 作為其他 Hook 的依賴:確保依賴的穩定性
// ? 適合使用 useMemo
const expensiveCalculation = useMemo(() => {return heavyProcessing(largeDataSet);
}, [largeDataSet]);// ? 避免創建新對象
const userInfo = useMemo(() => ({name: user.name,email: user.email,isActive: user.status === 'active'
}), [user.name, user.email, user.status]);
使用 useCallback 的時機
- 傳遞給子組件的函數:特別是使用了
React.memo
的子組件 - 作為 useEffect 的依賴:避免 effect 不必要的重新執行
- 自定義 Hook 中的函數:保持 API 的穩定性
// ? 適合使用 useCallback
const handleSubmit = useCallback((formData) => {submitForm(formData);
}, []);// ? 作為 useEffect 的依賴
const fetchData = useCallback(async () => {const data = await api.getData(id);setData(data);
}, [id]);useEffect(() => {fetchData();
}, [fetchData]);
常見誤區和注意事項
誤區 1:過度使用
不是所有的計算都需要使用 useMemo
,也不是所有的函數都需要使用 useCallback
。這些 Hook 本身也有開銷,只有在確實需要優化時才使用。
// ? 不需要優化的簡單計算
const simpleValue = useMemo(() => a + b, [a, b]);// ? 直接計算即可
const simpleValue = a + b;
誤區 2:依賴數組不正確
確保依賴數組包含所有使用到的變量,否則可能導致 bug。
// ? 缺少依賴
const memoizedValue = useMemo(() => {return calculate(a, b, c);
}, [a, b]); // 缺少 c// ? 完整的依賴
const memoizedValue = useMemo(() => {return calculate(a, b, c);
}, [a, b, c]);
誤區 3:在條件語句中使用
Hook 必須在函數組件的頂層調用,不能在條件語句中使用。
// ? 錯誤的使用方式
if (condition) {const value = useMemo(() => calculate(), []);
}// ? 正確的使用方式
const value = useMemo(() => {if (condition) {return calculate();}return defaultValue;
}, [condition]);
實際應用示例
讓我們看一個綜合使用 useMemo
和 useCallback
的實際例子:
import React, { useState, useMemo, useCallback, memo } from 'react';// 模擬昂貴的計算函數
const expensiveCalculation = (items) => {console.log('執行昂貴計算...');return items.reduce((sum, item) => sum + item.value * item.quantity, 0);
};// 子組件
const ProductItem = memo(({ product, onUpdate, onDelete }) => {console.log(`渲染產品: ${product.name}`);return (<div><span>{product.name}: {product.value} x {product.quantity}</span><button onClick={() => onUpdate(product.id)}>更新</button><button onClick={() => onDelete(product.id)}>刪除</button></div>);
});function ShoppingCart() {const [products, setProducts] = useState([{ id: 1, name: '商品A', value: 100, quantity: 2 },{ id: 2, name: '商品B', value: 200, quantity: 1 },{ id: 3, name: '商品C', value: 50, quantity: 3 }]);const [discount, setDiscount] = useState(0);// 使用 useMemo 緩存總價計算const totalPrice = useMemo(() => {const basePrice = expensiveCalculation(products);return basePrice * (1 - discount / 100);}, [products, discount]);// 使用 useCallback 緩存事件處理函數const handleUpdateProduct = useCallback((productId) => {setProducts(prev => prev.map(p => p.id === productId ? { ...p, quantity: p.quantity + 1 }: p));}, []);const handleDeleteProduct = useCallback((productId) => {setProducts(prev => prev.filter(p => p.id !== productId));}, []);return (<div><h2>購物車</h2>{products.map(product => (<ProductItemkey={product.id}product={product}onUpdate={handleUpdateProduct}onDelete={handleDeleteProduct}/>))}<div><label>折扣(%): <input type="number" value={discount}onChange={(e) => setDiscount(Number(e.target.value))}/></label></div><h3>總價: ¥{totalPrice.toFixed(2)}</h3></div>);
}