繁体   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