簡體   English   中英

React.memo 如何與 useCallback 一起使用

[英]How React.memo works with useCallback

據我了解,React.memo 是一個用於記憶組件的 API:如果它的 props 沒有改變,則 react 使用該組件的最新渲染,而不將其與之前的版本進行比較。 跳過新渲染並與舊渲染進行比較可以加快應用程序的速度。 涼爽的。

現在,這是我沒有得到的:如果道具沒有改變,那么一個未記憶的組件也不會被重新渲染,正如我從那個簡單的代碼中看到的那樣(使用這個鏈接查看演示,代碼片段on this page有點令人困惑) :普通組件+usecallback和memoized one+useCallback之間的渲染次數沒有區別。 基本上,我只需要 useCallbacks,因為普通組件不會使用相同的道具重新渲染。 那么,我錯過了什么? 當備忘錄幫助優化?

 const { useCallback, useEffect, useState, memo, useRef } = React; function Child({ fn, txt }) { const [state, setState] = useState(0); console.log(txt + " rendered!"); useEffect(() => { setState((state) => state + 1); }, [fn]); return ( <div style={{ border: "solid" }}> I'm a Child {!fn && <div>And I got no prop</div>} {fn && <div>And I got a fn as a prop</div>} <div> and I've got rendered <strong>{state}</strong> times </div> </div> ); } const MemoChild = memo(Child); function App() { const [, setState] = useState(true); const handlerOfWhoKnows = () => {}; return ( <div className="App"> I'm the parent <br /> <button onClick={() => setState((state) => !state)}> Change parent state </button> <h3>Ref</h3> ref: <Child txt="ref" fn={handlerOfWhoKnows} /> <h3>test</h3> useCB: <Child txt="useCb" fn={useCallback(handlerOfWhoKnows, [])} /> memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} /> memo + useCB: <MemoChild txt="memo+useCb" fn={useCallback(handlerOfWhoKnows, [])} /> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

該示例中發生了幾件事,但讓我們從標題開始:您的孩子標記為"memo+useCb"是完全正確的,它顯示了避免使用組件的正確方法(或至少正確的方法)不必要地重新渲染。 它正在做件必要的事情:

  1. 記憶組件(通過memo

  2. 確保組件的 props 不會發生不必要的變化(通過useCallback

你的其他人,那些標記為"useCb""memo"的人,每個人都缺少一半的必要因素:

  • "useCb"沒有被記憶,所以每次父渲染時它都會重新渲染

  • "memo"每次都獲得不同的fn道具,因此每次父級渲染時都會重新渲染

這是記憶和確保道具不改變(通過useCallbackuseMemouseRef等)的組合,避免了不必要的重新渲染。 他們中的任何一個都沒有,這就是您的示例所顯示的。

該示例可能會稍微誤導您的一件事是,您有組件說“我已經渲染了{state}次”,但state計算渲染次數,它計算fn道具的次數改變了價值,這不是一回事。 “渲染”是對函數組件的函數(或class組件的render方法)的調用。 在您的示例中,渲染次數由“useCb 渲染!”顯示。 和“備忘錄呈現!” 消息,每次父級呈現時都會看到,因為我們單擊了按鈕。

你說過(你的重點)

現在,這就是我沒有得到的:如果道具沒有改變,那么一個未記憶的組件也不會被重新渲染......

您的示例向您展示了未記憶的孩子確實會重新渲染,即使它的道具沒有改變: "useCb"版本具有穩定的道具,但每次父母渲染時它仍然會重新渲染。 您可以看到,因為它再次輸出“useCb 渲染!” 每次單擊導致父級重新渲染的按鈕時。

這是您示例的更新版本,希望在渲染發生時與道具更改發生時更清楚地顯示,使用日志記錄進行渲染並使用組件的渲染輸出進行道具更改:

 const { useCallback, useEffect, useState, memo, useRef } = React; function Child({ fn, txt }) { const [fnChanges, setFnChanges] = useState(0); const rendersRef = useRef(0); ++rendersRef.current; console.log(`${txt} rendered! (Render #${rendersRef.current})`); useEffect(() => { console.log(`${txt} saw new prop, will render again`); setFnChanges((changes) => changes + 1); }, [fn]); return ( <div style={{ border: "solid" }}> {txt}: <code>fn</code> changes: {fnChanges} </div> ); } const MemoChild = memo(Child); /*export default*/ function App() { const [, setState] = useState(true); const handlerOfWhoKnows = () => {}; const memoizedHandler = useCallback(handlerOfWhoKnows, []); return ( <div className="App"> <button onClick={() => setState((state) => !state)}>Change parent state</button> <div> Not memoizing anything: <Child txt="ref" fn={handlerOfWhoKnows} /> </div> <div> Just memoizing the <code>fn</code> prop, not the component: <Child txt="useCb" fn={memoizedHandler} /> </div> <div> Just memoizing the component, not the <code>fn</code> prop: <MemoChild txt="memo" fn={handlerOfWhoKnows} /> </div> <div> Memoizing <strong>both</strong> the component and the <code>fn</code> prop: <MemoChild txt="memo+useCb" fn={memoizedHandler} /> </div> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

但是,很難清楚地看到日志記錄,所以這里有第二個版本,它在組件的渲染輸出中包含了渲染的數量(在渲染結果中包含非狀態是非常不尋常的,但用於說明目的在這種情況下,看看發生了什么):

 const { useCallback, useEffect, useState, memo, useRef } = React; function Child({ fn, txt }) { const [fnChanges, setFnChanges] = useState(0); const rendersRef = useRef(0); ++rendersRef.current; useEffect(() => { setFnChanges((changes) => changes + 1); }, [fn]); return ( <div style={{ border: "solid" }}> {txt}: Renders: {rendersRef.current}, <code>fn</code> changes: {fnChanges} </div> ); } const MemoChild = memo(Child); /*export default*/ function App() { const [, setState] = useState(true); const handlerOfWhoKnows = () => {}; const memoizedHandler = useCallback(handlerOfWhoKnows, []); return ( <div className="App"> <button onClick={() => setState((state) => !state)}>Change parent state</button> <div> Not memoizing anything: <Child txt="ref" fn={handlerOfWhoKnows} /> </div> <div> Just memoizing the <code>fn</code> prop, not the component: <Child txt="useCb" fn={memoizedHandler} /> </div> <div> Just memoizing the component, not the <code>fn</code> prop: <MemoChild txt="memo" fn={handlerOfWhoKnows} /> </div> <div> Memoizing <strong>both</strong> the component and the <code>fn</code> prop: <MemoChild txt="memo+useCb" fn={memoizedHandler} /> </div> </div> ); } const root = ReactDOM.createRoot(document.getElementById("root")); root.render(<App />);
 <div id="root"></div> <script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>

同樣,您的示例確實顯示了使用"memo+useCb"子項的正確方法。

除了@TJ Crowder 的回答,

您的示例項目在<StrictMode>中運行,因此 React 將在開發模式下運行一個組件兩次以驗證事物。 要理解這個問題,首先讓我們暫時從index.js中刪除<ScrictMode

基本上,我只需要 useCallbacks,因為普通組件不會使用相同的道具重新渲染。 那么,我錯過了什么? 當備忘錄幫助優化?

通過使用memo ,您告訴協調算法如果道具相同,則不要繼續渲染子組件。 但是在沒有memo的普通組件中,協調算法將遍歷樹並調用您的子組件(因為這是算法應該做的)。 遍歷(渲染)完成后,react 會將更改刷新到 DOM。 因為沒有任何更改將提交給 DOM。 但是使用memo可以加快遍歷過程。


筆記:

在 React 中,“渲染”並不意味着更新 DOM 元素。 渲染意味着調用你的函數組件或 Component.render() 方法。 這不是一個昂貴的過程,因此 React 會這樣做,但 React 不會盲目地將更改刷新到 DOM。 您可以通過打開檢查元素來驗證這一點。 當您展開檢查元素時,單擊按鈕,您不會看到任何更改(在 dom 元素上創建/刪除帶有紫色突出顯示的動畫)。 通過使用備忘錄,您可以手動停止 render() 並移動到兄弟元素(如果可用)以繼續重新渲染而不是進入子元素。 這也是為什么您不能使用console.log來實際測試 react 應用程序的性能的原因。 console.log是一個副作用。 React 嚴格告訴你不要在渲染階段(函數體的頂層)添加副作用。 要處理副作用,您需要使用useEffect


現在讓我們看看您的代碼中的問題,根據您的沙盒代碼,在刪除<StrictMode>並重新加載預覽后,我們將在控制台上獲得以下日志,

對於這個組件,

    <div className="App">
      ref: <Child txt="ref" fn={handlerOfWhoKnows} />
      useCB: <Child txt="useCb" fn={useCallback(handlerOfWhoKnows, [])} />
      memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
      memo + useCB: <MemoChild txt="memo+useCb" fn={useCallback(handlerOfWhoKnows, [])} />
    </div>

它將在安裝時記錄以下內容,

ref rendered!
useCb rendered! 
memo rendered! 
memo+useCb rendered!

ref rendered! 
useCb rendered! 
memo rendered! 
memo+useCb rendered! 

1.在mount時記錄打印兩次

您看到每個日志打印兩次的原因是,您在<Child/>組件中使用了useEffect

  useEffect(() => {
    setState((state) => state + 1);
  }, [fn]);

由於觸發了狀態更改,React 也將第二次重新運行<Child /> 跟道具沒關系。 如果我們刪除這個useEffect ,現在你可以看到掛載日志,

ref rendered! 
useCb rendered! 
memo rendered! 
memo+useCb rendered! 

2. 點擊按鈕

刪除子項中的StrictModeuseEffect后,單擊按鈕。 這次將打印日志

ref rendered! // rendered due to the prop changed
useCb rendered! // rendered due to the prop changed
memo rendered! // rendered due to the prop changed

這次可以看到,沒有打印memo+useCb 下一個問題是為什么,

3. 為什么要memo rendered! 在第二步。

這是因為您沒有記住fn ,所以每次渲染時它都會重新創建。 所以 props 改變 -> 組件重新渲染

所以代碼應該是,

const handlerOfWhoKnows = useCallback(() => {}, []);

<MemoChild txt="memo" fn={handlerOfWhoKnows} />

現在隨着更改,組件看起來像,

  const handlerOfWhoKnows = useCallback(() => {}, []);

  return (
    <div className="App">
      ref: <Child txt="ref" fn={handlerOfWhoKnows} />
      useCB: <Child txt="useCb" fn={handlerOfWhoKnows} />
      memo: <MemoChild txt="memo" fn={handlerOfWhoKnows} />
      memo + useCB: <MemoChild txt="memo+useCb" fn={handlerOfWhoKnows} />
    </div>
  );

現在當我們點擊按鈕時,我們只會看到,

ref rendered! 
useCb rendered! 

所以你的問題的答案,

當備忘錄幫助優化?

如果道具相同,備忘錄有助於手動停止進一步的渲染。 一旦發現memo和道具相同,對賬算法將停止遍歷。 它將完成渲染樹或繼續渲染兄弟元素。

過多地使用備忘錄也會導致性能問題,因為memo本身會執行一些過程來確定是否渲染子節點。 謹慎使用useCallbackuseMemo

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM