简体   繁体   English

React.memo 如何与 useCallback 一起使用

[英]How React.memo works with useCallback

As far as I understand, React.memo is an API that memoize a component: if its props don't change, react use the latest render of that component without comparing it with its previous version.据我了解,React.memo 是一个用于记忆组件的 API:如果它的 props 没有改变,则 react 使用该组件的最新渲染,而不将其与之前的版本进行比较。 Skipping new render and comparing with old one speed-up the app.跳过新渲染并与旧渲染进行比较可以加快应用程序的速度。 Cool.凉爽的。

Now, here's what I don't get: if props don't change, also a not memoized component don't get re-rendered , as I can see from that simple code ( use this link to see the demo, the code snippet on this page is a little bit confusing) : there's no difference about number of renders between a normal component+usecallback and a memoized one+useCallback.现在,这是我没有得到的:如果道具没有改变,那么一个未记忆的组件也不会被重新渲染,正如我从那个简单的代码中看到的那样(使用这个链接查看演示,代码片段on this page有点令人困惑) :普通组件+usecallback和memoized one+useCallback之间的渲染次数没有区别。 Basically, useCallbacks is all I need, as a normal component doesn't get re-rendered with same props.基本上,我只需要 useCallbacks,因为普通组件不会使用相同的道具重新渲染。 Then, what I'm I missing?那么,我错过了什么? When memo comes in help to optimize?当备忘录帮助优化?

 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>

There are a couple of things going on in that example, but let's start with the headline: Your child labelled "memo+useCb" is exactly right, it's showing the correct way (or at least, a correct way) to avoid having a component re-render unnecessarily.该示例中发生了几件事,但让我们从标题开始:您的孩子标记为"memo+useCb"是完全正确的,它显示了避免使用组件的正确方法(或至少正确的方法)不必要地重新渲染。 It's doing both necessary things:它正在做件必要的事情:

  1. Memoizing the component (via memo )记忆组件(通过memo

    and

  2. Ensuring that the component's props don't change unnecessarily (via useCallback )确保组件的 props 不会发生不必要的变化(通过useCallback

Your others, the ones labelled "useCb" and "memo" , each lack half of the necessary incredients:你的其他人,那些标记为"useCb""memo"的人,每个人都缺少一半的必要因素:

  • "useCb" isn't memoized, so it re-renders every time the parent renders "useCb"没有被记忆,所以每次父渲染时它都会重新渲染

  • "memo" is getting a different fn prop every time, so it re-renders every time the parent renders "memo"每次都获得不同的fn道具,因此每次父级渲染时都会重新渲染

It's the combination of memoizing and ensuring the props don't change (via useCallback , or useMemo , or useRef , etc.) that avoids unnecessary re-renders.这是记忆和确保道具不改变(通过useCallbackuseMemouseRef等)的组合,避免了不必要的重新渲染。 Either of them, alone, doesn't, which is what your example is showing.他们中的任何一个都没有,这就是您的示例所显示的。

One thing about the example that may be misleading you slightly is that you have the components saying "and I've got rendered {state} times" but state isn't counting the number of renders, it's counting the number of times the fn prop changed value, which is not the same thing.该示例可能会稍微误导您的一件事是,您有组件说“我已经渲染了{state}次”,但state计算渲染次数,它计算fn道具的次数改变了价值,这不是一回事。 A "render" is a call to your function component's function (or the render method of a class component). “渲染”是对函数组件的函数(或class组件的render方法)的调用。 In your example, the number of renders is shown by the "useCb rendered!"在您的示例中,渲染次数由“useCb 渲染!”显示。 and "memo rendered!"和“备忘录呈现!” messages, which you see every time the parent renders because we click the button.消息,每次父级呈现时都会看到,因为我们单击了按钮。

You've said (your emphasis) :你说过(你的重点)

Now, here's what I don't get: if props don't change, also a not memoized component don't get re-rendered ...现在,这就是我没有得到的:如果道具没有改变,那么一个未记忆的组件也不会被重新渲染......

Your example is showing you that the un-memoized child does get re-rendered, even when its props don't change: the "useCb" version has stable props, but it still re-renders every time the parent renders.您的示例向您展示了未记忆的孩子确实会重新渲染,即使它的道具没有改变: "useCb"版本具有稳定的道具,但每次父母渲染时它仍然会重新渲染。 You can see that because, again, it outputs "useCb rendered!"您可以看到,因为它再次输出“useCb 渲染!” every time you click the button causing the parent to re-render.每次单击导致父级重新渲染的按钮时。

Here's an updated version of your example hopefully showing more clearly when a render happens vs. when a prop change happens, using logging for renders and the component's rendered output for prop changes:这是您示例的更新版本,希望在渲染发生时与道具更改发生时更清楚地显示,使用日志记录进行渲染并使用组件的渲染输出进行道具更改:

 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>

It can be hard to see the logging clearly, though, so here's a second version that includes the number of renders in the component's rendered output (it's very unusual to include non-state in the rendered result, but helpful just for the purposes of illustration in this case, to see what's going on):但是,很难清楚地看到日志记录,所以这里有第二个版本,它在组件的渲染输出中包含了渲染的数量(在渲染结果中包含非状态是非常不寻常的,但用于说明目的在这种情况下,看看发生了什么):

 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>

So again, your example did show the right way to do it, with the "memo+useCb" child.同样,您的示例确实显示了使用"memo+useCb"子项的正确方法。

Additional to the @TJ Crowder's answer,除了@TJ Crowder 的回答,

Your sample project runs in <StrictMode> so React will run a component twice in dev mode to validate things.您的示例项目在<StrictMode>中运行,因此 React 将在开发模式下运行一个组件两次以验证事物。 To understand the issue, first lets remove <ScrictMode temporally from the index.js要理解这个问题,首先让我们暂时从index.js中删除<ScrictMode

Basically, useCallbacks is all I need, as a normal component doesn't get re-rendered with same props.基本上,我只需要 useCallbacks,因为普通组件不会使用相同的道具重新渲染。 Then, what I'm I missing?那么,我错过了什么? When memo comes in help to optimize?当备忘录帮助优化?

By using memo you are telling the reconciliation algorithm to NOT to go down and render the Child component again if the props are same.通过使用memo ,您告诉协调算法如果道具相同,则不要继续渲染子组件。 But in a normal component without memo , the reconciliation algorithm will traverse through the tree and call your child component ( because that's what algorithm supposed to do ).但是在没有memo的普通组件中,协调算法将遍历树并调用您的子组件(因为这是算法应该做的)。 Once the traversing(rendering) is done, react will flush the changes to the DOM.遍历(渲染)完成后,react 会将更改刷新到 DOM。 As there's no changes will be committed to the DOM.因为没有任何更改将提交给 DOM。 but using memo you are speed up the traversing process.但是使用memo可以加快遍历过程。


NOTE:笔记:

In React, 'render' doesn't mean updating the DOM elements.在 React 中,“渲染”并不意味着更新 DOM 元素。 rendering mean calling your function component or Component.render() method.渲染意味着调用你的函数组件或 Component.render() 方法。 This is not an expensive process so React will do it but React won't blindly flush the changes to the DOM.这不是一个昂贵的过程,因此 React 会这样做,但 React 不会盲目地将更改刷新到 DOM。 You can validate this by open the inspect element.您可以通过打开检查元素来验证这一点。 When you expand the inspect element, click the button, you won't see any changes(create/delete animation with purple color highlight on dom elements) happens.当您展开检查元素时,单击按钮,您不会看到任何更改(在 dom 元素上创建/删除带有紫色突出显示的动画)。 By using memo you manually stop the render() and move to the sibling element ( if available ) to continue re render instead of going into the child element.通过使用备忘录,您可以手动停止 render() 并移动到兄弟元素(如果可用)以继续重新渲染而不是进入子元素。 This is also why you cannot use console.log to actually test the performance of the react app.这也是为什么您不能使用console.log来实际测试 react 应用程序的性能的原因。 console.log is a side effect. console.log是一个副作用。 React strictly telling you to not add side effect to the render phase ( top level of your function body ). React 严格告诉你不要在渲染阶段(函数体的顶层)添加副作用。 To handle side effects you need to use useEffect .要处理副作用,您需要使用useEffect


Now lets see the issues in your code, Based on your sandbox code, after removing <StrictMode> and reload the preview, we will get the following logs on the console,现在让我们看看您的代码中的问题,根据您的沙盒代码,在删除<StrictMode>并重新加载预览后,我们将在控制台上获得以下日志,

For this component,对于这个组件,

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

It will log following on mount,它将在安装时记录以下内容,

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

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

1. log print twice on mount 1.在mount时记录打印两次

The reason why you see each logs print twice is, you use a useEffect inside your <Child/> component.您看到每个日志打印两次的原因是,您在<Child/>组件中使用了useEffect

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

Since the state change triggered, React will re run the <Child /> for the 2nd time as well.由于触发了状态更改,React 也将第二次重新运行<Child /> It has nothing to do with the props.跟道具没关系。 If we remove this useEffect , now you can see the mount log as,如果我们删除这个useEffect ,现在你可以看到挂载日志,

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

2. Clicking the button 2. 点击按钮

After remove the StrictMode and useEffect in the child, and click the button.删除子项中的StrictModeuseEffect后,单击按钮。 This time the log will print这次将打印日志

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

This time you can see, memo+useCb is not printed.这次可以看到,没有打印memo+useCb The next issue is why,下一个问题是为什么,

3. Why memo rendered! 3. 为什么要memo rendered! in 2nd step.在第二步。

This is because you didn't memoized the fn so it will recreate everytime when rendering.这是因为您没有记住fn ,所以每次渲染时它都会重新创建。 So props change -> component re render所以 props 改变 -> 组件重新渲染

So code should be,所以代码应该是,

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

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

Now with the changes, the component will looks like,现在随着更改,组件看起来像,

  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>
  );

Now when we click the button, we will only see,现在当我们点击按钮时,我们只会看到,

ref rendered! 
useCb rendered! 

So the answer for your question,所以你的问题的答案,

When memo comes in help to optimize?当备忘录帮助优化?

memo helps to manually stop the further rendering if the props are same.如果道具相同,备忘录有助于手动停止进一步的渲染。 Reconciliation algorithm will stop traversing once it see a memo and props are same.一旦发现memo和道具相同,对账算法将停止遍历。 It will complete the rendering the tree or it will continue to render the sibling element.它将完成渲染树或继续渲染兄弟元素。

Also using the memo too much can lead to performance issues as well because memo it self does some process to figure out whether to render the child or not.过多地使用备忘录也会导致性能问题,因为memo本身会执行一些过程来确定是否渲染子节点。 Use useCallback and useMemo carefully.谨慎使用useCallbackuseMemo

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM