React.Memo与UseCallback一起使用

发布于 2025-02-11 03:34:08 字数 2324 浏览 2 评论 0 原文

据我所知,react.memo是一种记忆组件的API:如果其道具不更改,则React使用该组件的最新渲染,而无需将其与以前的版本进行比较。跳过新的渲染并与旧的One加速应用程序进行比较。凉爽的。

现在,这是我没有得到的:如果道具不变,那么一个未回忆的组件也不会重新渲染,正如我从该简单代码使用此链接查看演示,代码,该链接此页面上的摘要有点令人困惑):正常组件+usecallback和一个回忆的一个+usecallback之间的渲染数没有区别。基本上,我需要的是用户验证,因为普通组件不会通过相同的道具重新渲染。 那我想念什么?何时备忘录来优化?

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>

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. 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. Basically, useCallbacks is all I need, as a normal component doesn't get re-rendered with same props.
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>

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

忆沫 2025-02-18 03:34:08

该示例中有几件事,但让我们从标题开始:您的孩子标记为“ Memo+USECB” 是完全正确的,它显示了正确的方式(或者至少, a 正确的方式)避免了不必要的组件重新渲染。它在做兼有必要的事情:

  1. 纪念组件(通过 Memo

  2. 确保组件的道具不会不必要地更改(通过 usecallback

您的其他设备,将其标记为“ usecb” “ memo” ,每个标记为 缺少一半必要的不可能:

  • “ usecb” 没有记忆,因此每次父母呈现时,它都会重新呈现

  • “ usecb” 没有记忆,因此每当父派渲染

    “ memo” 获得不同的 fn 每次支架,因此每次父母呈现

    时都会重新呈现

它都会重新呈现组合的记忆和确保这些道具不会更改(通过 USECALLBACK usememo useref 等),以避免不必要的重新订阅者。他们一个人都不是,这是您的示例所显示的。

关于这个示例可能会稍微误导您的一件事是,您的组件说“并且我已经渲染了 {state} times”,但是 state> state ISN 't 计算渲染数的数量,它计算 fn prop更改值的次数,这不是同一件事。 “渲染”是对您功能组件函数的调用(或 class component的 Render 方法)。在您的示例中,渲染的数量由“ USECB渲染!”显示。和“备忘录渲染!”消息,您会看到父母呈现的每个时间,因为我们单击按钮。

您已经说过(您的重点)

现在,这是我没有得到的:如果道具不变,那么一个不记忆的组件也不会重新渲染 ...

您的示例表明您是未遗忘的孩子 do 即使它的道具不变,也会重新渲染:“ usecb” 版本具有稳定的道具,但是每次父母呈现时,它仍然会重新呈现。您可以看到这一点,因为它再次输出“ USECB渲染!”每次单击按钮时,导致父母重新渲染。

这是您示例的更新版本,希望在A 渲染发生时更清楚地显示出更清晰的版本,而在发生Prop更改时,使用记录来进行渲染,以及组件的渲染输出进行Prop Change:

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” Child。

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. It's doing both necessary things:

  1. Memoizing the component (via memo)

    and

  2. Ensuring that the component's props don't change unnecessarily (via useCallback)

Your others, the ones labelled "useCb" and "memo", each lack half of the necessary incredients:

  • "useCb" isn't memoized, so it re-renders every time the parent renders

  • "memo" is getting a different fn prop every time, so it re-renders every time the parent renders

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. 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. A "render" is a call to your function component's function (or the render method of a class component). In your example, the number of renders is shown by the "useCb rendered!" 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. You can see that because, again, it outputs "useCb rendered!" 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.

滿滿的愛 2025-02-18 03:34:08

除@TJ Crowder的答案外,

您的示例项目还以&lt; strictmode&gt; 运行,因此React将在 dev 模式下两次运行一个组件来验证事物。要了解问题,首先让我们从 index.js 暂时删除&lt; scrictmode

基本上,我需要的是usecallbacks,因为普通组件没有
通过相同的道具重新渲染。那我想念什么?备忘录
有帮助以优化?

通过使用备忘录,您告诉对帐算法,如果道具相同,请不要下降并再次渲染子组件。但是在没有备忘录的普通组件中,对帐算法将穿越树并调用您的子组件(因为那是算法应该做的)。一旦完成遍历(渲染),React将把更改与DOM冲洗。由于没有任何更改会对DOM进行。但是,使用备忘您正在加快遍历过程。


注:

在React中,'渲染'并不意味着更新DOM元素。渲染是指调用您的函数组件或component.render()方法。这不是一个昂贵的过程,因此React会这样做,但是React不会盲目地将变化冲洗给DOM。您可以通过打开检查元素来验证这一点。当您展开检查元素时,单击按钮,您将不会看到任何更改(创建/删除DOM元素上的紫色颜色亮点的动画)会发生。
通过使用备忘录,您可以手动停止渲染()并移至兄弟姐妹(如果有)继续重新渲染而不是进入子元素。这也是为什么您不能使用 console.log 实际测试React应用程序的性能。
console.log 是副作用。严格地告诉您不要为渲染阶段(功能主体的最高级别)添加副作用。 您需要使用使用


要处理副作用, 重新加载预览,我们将在控制台上获取以下日志,

对于此组件,

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

它将在MOUTE上登录

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

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

1。在坐骑上登录两次,

这是您看到每个日志打印两次的原因,您使用 pers a 使用> 在您的&lt; child/&gt; 组件中。

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

由于触发了状态变化,因此第二次也将在第二次运行&lt; child /&gt; < /code>。它与道具无关。如果我们删除此 useffect ,现在您可以将安装log看到为

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

单击按钮。

2。在孩子中删除 strictmmode stricteffect 后 ,然后单击按钮。这次将打印

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。为什么在第二步中呈现备忘录!

这是因为您没有回忆 fn ,因此每次渲染时都会重新创建。因此,道具变更 - &gt;组件重新渲染,

因此,

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! 

,所以您的问题的答案,

何时备忘录以优化?

如果道具相同,备忘录有助于手动停止进一步的渲染。一旦看到备忘录,对帐算法将停止遍历,道具相同。它将完成渲染树,否则将继续渲染兄弟姐妹。

同样,使用备忘录太多也可能导致性能问题,因为备忘它会做一些过程来弄清楚是否渲染孩子。使用 usecallback usememo 仔细。

Additional to the @T.J Crowder's answer,

Your sample project runs in <StrictMode> so React will run a component twice in dev mode to validate things. To understand the issue, first lets remove <ScrictMode temporally from the index.js

Basically, useCallbacks is all I need, as a normal component doesn't
get re-rendered with same props. 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. 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 ). Once the traversing(rendering) is done, react will flush the changes to the DOM. As there's no changes will be committed to the DOM. but using memo you are speed up the traversing process.


NOTE:

In React, 'render' doesn't mean updating the DOM elements. rendering mean calling your function component or Component.render() method. This is not an expensive process so React will do it but React won't blindly flush the changes to the 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.
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. This is also why you cannot use console.log to actually test the performance of the react app.
console.log is a side effect. React strictly telling you to not add side effect to the render phase ( top level of your function body ). To handle side effects you need to use 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,

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

The reason why you see each logs print twice is, you use a useEffect inside your <Child/> component.

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

Since the state change triggered, React will re run the <Child /> for the 2nd time as well. It has nothing to do with the props. If we remove this useEffect, now you can see the mount log as,

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

2. Clicking the button

After remove the StrictMode and useEffect in the child, and click the button. 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. The next issue is why,

3. Why memo rendered! in 2nd step.

This is because you didn't memoized the fn so it will recreate everytime when rendering. So props change -> component re render

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. 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. Use useCallback and useMemo carefully.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文