
本文基于一個真實踩坑記錄展開帶你徹底理解“為什么明明包了 memo子組件還是跟著父組件亂渲染”。一個典型的性能浪費現場先看你提供的代碼它非常直觀地暴露了問題import { useState, memo } from react; function RegularChild({ name }) { console.log(渲染了RegularChild組件); return h1{name}/h1; } const MemoChild memo(({ name }) { console.log(MemoChild 組件渲染了); return h1Hello, {name}/h1; }); function App() { const [count, setCount] useState(0); const [name, setName] useState(少林隊); return ( button onClick{() setCount(count 1)}點擊計數{count}/button button onClick{() setName(峨眉隊)}改變名字/button RegularChild name{name} / MemoChild name{name} / / ); }運行一下你會發現點擊“點擊計數”按鈕count變化了name沒變但是RegularChild和MemoChild都會重新渲染。只有當你再次點擊“改變名字”MemoChild才會再渲染一次因為name確實變了而RegularChild一直傻傻跟著父組件渲染。這里就引出了核心思路React 的渲染是自上而下的“瀑布流”父組件更新默認會帶著所有子組件一起更新。如果你不做任何干預性能優化就無從談起。React.memo組件級別的淺比較盾牌memo的作用很簡單給你一個高階組件它會對傳入的 props 進行淺比較如果 props 沒變就跳過渲染直接復用上一次的結果。在你的代碼中MemoChild正是憑借memo在count變化而name沒變時成功避免了無效渲染——這已經是一次勝利。但是故事到這里才剛剛開始。很多開發者會掉進下面這個陷阱當 props 里有函數或對象時memo 直接“破防”咱們稍微改一下代碼給MemoChild加一個onClick回調const MemoChild memo(({ name, onUpdate }) { console.log(MemoChild 渲染了); return ( h1Hello, {name}/h1 button onClick{onUpdate}內部改名/button / ); }); function App() { const [count, setCount] useState(0); const [name, setName] useState(少林隊); // 父組件里定義了一個回調函數 const handleUpdate () { setName(內部更新的隊名); }; return ( button onClick{() setCount(count 1)}點擊計數{count}/button MemoChild name{name} onUpdate{handleUpdate} / / ); }猜一下點擊“點擊計數”按鈕MemoChild會渲染嗎答案是會盡管name沒變MemoChild依然重渲染了你的memo盾牌好像被什么東西擊穿了。原因很簡單每次App組件渲染里面的handleUpdate都是一個全新的函數引用。對于MemoChild來說onUpdate這個 prop 的引用變了淺比較就判定為“props 變化了”于是照常渲染。memo 很忠誠但它只看引用——函數、對象、數組只要引用不同就會觸發渲染。這才是性能優化的暗礁區。為什么函數每次都是“全新的引用”你可能會有疑問明明函數內容一模一樣為什么每次都會被當成不同的東西這要從 React 函數組件的本質說起。函數組件就是一個普通的 JavaScript 函數每次狀態更新React 都會重新調用這個函數生成一份全新的“快照”。你寫的handleUpdate是在函數體內部用const聲明的const handleUpdate () { setName(內部更新的隊名); };這行代碼每執行一次就會在內存里新創建一個函數對象并給handleUpdate分配一個新的地址。即便兩個函數的內容完全一樣它們在內存中的位置也不同。就像一個高級私房菜館你每次都拿一張臨時手寫的點菜單老板看的是紙本身不是上面的內容——紙不一樣就按新單子處理。對于memo來說它比較 props 時用的是淺比較基本類型比“值”引用類型比“地址”。onUpdate是一個函數屬于引用類型所以只要地址變了memo就會認為 prop 更新了于是老老實實渲染子組件。你可以直接在代碼里加上一行驗證console.log(handleUpdate 引用:, handleUpdate);每點一次按鈕控制臺顯示的“地址標識”都會發生變化。useCallback給函數一個“固定住”的引用要解決這個問題我們就需要讓handleUpdate在多次渲染之間保持同一個地址除非它真正依賴的某個值變化了。useCallback就是干這個的const handleUpdate useCallback(() { setName(內部更新的隊名); }, []);useCallback會在依賴項這里是空數組[]不變的情況下直接返回上一次緩存的函數引用。現在無論你點擊“點擊計數”多少次handleUpdate指向的都是同一塊內存地址memo淺比較通過子組件也就不會被帶著跑了。一句話總結當你需要把函數傳給用memo包裹的子組件時請用useCallback包裹它否則函數引用的變化會讓memo形同虛設。每次渲染都產生新函數舊地址去哪了會不會內存泄漏你可能會擔心既然每次渲染都要新建函數那舊函數不會被垃圾清理掉嗎會不會內存越用越多放心舊函數會被 JavaScript 的垃圾回收機制自動清理。只要上一輪渲染產生的handleUpdate沒有被其他地方“抓住”比如傳入全局變量、未清理的定時器或者閉包泄漏它就變成了“不可達對象”垃圾回收器會在合適的時機回收它占用的內存。useCallback的主要目的并不是節省內存事實上它反而需要多占用一點點緩存空間而是保持引用穩定從而避開memo的無效渲染。這點開銷在避免昂貴渲染的收益面前完全值得。React 不會讓舊函數堆積成山。它只是在每次渲染時短暫存在隨后被清掃你真正要關心的是“引用變化”帶來的重渲染而不是內存泄漏。當然如果你在useEffect里錯誤地添加了未清理的監聽器且監聽了某個內聯函數那確實有可能造成內存泄漏但那是另一個需要避免的坑了和這里的場景無關。useMemo不只是緩存函數引用還能緩存任何值useMemo和useCallback原理相似但適用范圍更廣useCallback緩存函數本身useMemo緩存函數執行的結果可以是任意值最常見的一個場景昂貴的計算 子組件傳遞。const MemoList memo(({ data }) { console.log(MemoList 渲染了); return div{data.join(,)}/div; }); function App() { const [count, setCount] useState(0); const [items] useState([1, 2, 3, 4, 5]); // 假設這是一個復雜過濾/排序邏輯 const processedData items.filter(item item 2).sort((a, b) b - a); return ( button onClick{() setCount(count 1)}計數{count}/button MemoList data{processedData} / / ); }點擊按鈕MemoList依然重渲染。因為processedData在每次渲染時都是一個新數組引用。哪怕內容完全一樣淺比較也會認為它變了。這時候就該useMemo出場了const processedData useMemo(() { return items.filter(item item 2).sort((a, b) b - a); }, [items]); // items 不變結果引用就不變現在processedData的引用在items不變時會保持穩定memo(子組件)也就能正常工作。另附一個經典踩坑有很多人直接用useMemo緩存 JSX比如const child useMemo(() MemoChild name{name} /, [name]); return {child}/;這確實能避免子組件不必要的渲染但代碼可讀性差且破壞了 React 的 diff 邏輯。原則上更推薦用memo包裹子組件 useCallback/useMemo穩定 props而不是用useMemo直接把 JSX 緩存掉。它們之間的配合關系memo是子組件側的防御機制props 不變我就不渲染。useCallback和useMemo是父組件側的穩定工具確保傳給子組件的 props函數、對象、數組引用不變。三者配合才能真正做到“不相關狀態更新時子組件紋絲不動”。再補一句很多人忽視的事實useCallback和useMemo本身也有開銷。如果你傳給的只是一個普通div或者根本沒有被memo包裹的子組件那反而是一種浪費。它們只在結合memo使用或作為其他 Hook 的依賴項時才真正發揮威力。一個“可落地”的優化步驟 checklist如果你在項目中遇到某個頁面渲染卡頓或者子組件無緣無故跟著父親亂抖可以按下面的步驟排查確認渲染范圍用console.log或者 React DevTools 的 Highlight 功能看哪些組件在不必渲染時也渲染了。對純展示、且依賴穩定的子組件包上memo注意只對 props 基本穩定的組件做別一股腦全包。檢查傳給子組件的引用類型 props如果有函數、對象、數組看看它們在父組件中是否每次渲染都重新創建。用useCallback穩定函數用useMemo穩定對象/數組/計算結果依賴項要寫準確避免閉包陷阱比如忘了依賴某個 state。確認優化有效再用 console 或 DevTools profiler 驗證一下子組件是否真的跳過了渲染。最后說一個容易被忽略的思維升級很多教程會說useCallback和useMemo是用來“緩存函數”和“緩存計算結果”的但它們真正的定位是引用穩定性。你的目標是讓那些被memo保護的子組件收到的 props 引用不要無意義地變化從而避免重渲染。寫性能優化的代碼本質上是在管理“變化” —— 你告訴 React 什么真正變化了什么只是看起來變化了。memo 是盾useCallback/useMemo 是矛你要把矛頭準確扎向“假變化”。