【问题标题】:Why doesn't memoization of a React functional component call with useMemo() hook work?为什么使用 useMemo() 挂钩的 React 功能组件调用的记忆不起作用?
【发布时间】:2021-07-05 04:19:43
【问题描述】:

好的,我想我不妨在这里补充一下,我知道代码看起来不太好,但我认为它可以完成这项工作,所以如果你能解释为什么不只是它,我会很高兴如果它有效,那将不是一个好习惯,但为什么它不起作用。我也很高兴听到您对问题的解决方案!

我很难理解为什么下面的代码块似乎没有按预期工作,我的意思是记住 <First /><Second /> 功能组件的返回值,而不是在每个 @ 上调用它们的函数987654325@ 渲染。我认为由于<SomeComponent /> 表达式返回一个对象,因此可以简单地记住它并继续使用。似乎不起作用,我不知道为什么。

顺便说一句,如果您能解释一下为什么渲染 <App /> 组件会导致 renders.current 值增加 2,而只调用一次 console.log,我将不胜感激。

非常感谢您的帮助!

import "./styles.css";
import React from "react";

const First = () => {
  const renders = React.useRef(0);
  renders.current += 1;
  console.log('<First /> renders: ', renders.current);

  return <h1>First</h1>;
}

const Second = () => {
  const renders = React.useRef(0);
  renders.current += 1;
  console.log('<Second /> renders: ', renders.current);

  return <h1>Second</h1>;
}

const App = () => {
  const [isSwapped, setIsSwapped] = React.useState(false);
  const [, dummyState] = React.useState(false);

  const first = React.useMemo(() => <First />, []);
  const second = React.useMemo(() => <Second />, []);

  const renders = React.useRef(0);
  renders.current += 1;
  console.log('<App /> renders: ', renders.current);

  return (
    <div className="App">
      <button onClick={() => setIsSwapped((isSwapped) => !isSwapped)}>Swap</button>
      <button onClick={() => dummyState((state) => !state)}>Re-render</button>
      {isSwapped ? (
        <>
          {first}
          {second}
        </>
      ) : (
        <>
          {second}
          {first}
        </>
      )}
    </div>
  );
}

编辑:感谢您的回复,伙计们,这是符合预期的版本:https://codesandbox.io/s/why-doesnt-memoization-of-a-react-functional-component-call-with-usememo-hook-forked-gvnn0?file=/src/App.js

【问题讨论】:

  • 当您在组件主体中执行一堆副作用(引用计数、控制台日志记录)时,您会得到奇怪的结果。 useMemo 钩子用于记忆值,而不是组件,为此使用 memo 高阶组件。
  • 但是功能组件调用的返回不只是另一个值吗?我知道代码不是惯用的,但我不明白为什么它不起作用。此外,在这种情况下,使用 React.memo() 不会改变行为,所以对我来说,这里发生了什么的问题仍然悬而未决
  • 嗯,一切都是一个值。你所有的代码都是非常反模式的(虽然你的布尔切换正确,所以 +1 ),你的问题基本上是“为什么我的代码不起作用?”这是因为你没有真正遵循任何既定的 React 准则。如果你想记住一个 React 组件,memo Higher Order Component 就是答案。关于控制台日志和引用突变的其余问题是无意的副作用,因为您没有在生命周期中运行 that 代码,即 useEffect
  • 好吧,感谢您的回复,我只是想知道为什么这个代码是反模式的,以及在引擎盖下发生了什么并不能使它工作。我并不怀疑它不好,但我的问题是为什么会这样。 “因为它不遵循准则”- 这些准则可能更多是我希望理解的一些推理的结果。
  • 而且,正如我之前所说,备忘录 HOC 在这里不起作用

标签: reactjs react-hooks


【解决方案1】:

我尝试使用通过 useMemo 钩子记住的组件返回值的代码,然后用 React.memo 包装每个组件,结果对我来说似乎是一样的。

使用memo高阶组件来装饰组件会记忆渲染结果。

如果你的组件在给定相同的 props 的情况下呈现相同的结果,你 可以将其封装在对 React.memo 的调用中,以提高某些性能 通过记忆结果的案例。这意味着 React 将跳过 渲染组件,并重用上次渲染的结果。

const First = React.memo(() => {
  const renders = React.useRef(0);

  React.useEffect(() => {
    renders.current += 1;
    console.log("<First /> renders: ", renders.current);
  });

  return <h1>First</h1>;
});

const Second = React.memo(() => {
  const renders = React.useRef(0);

  React.useEffect(() => {
    renders.current += 1;
    console.log("<Second /> renders: ", renders.current);
  });

  return <h1>Second</h1>;
});

您会看到我还在以下问题中解决了无意的副作用(控制台日志和 ref 突变):

顺便说一句,如果你能帮助我,那也很酷 了解为什么父组件似乎将+2 添加到 renders.current 在每次渲染时,即使我放了一个 console.log('render') 在组件中,只会出现一次。

这是因为组件生命周期有两个阶段,“渲染阶段”和“提交阶段”。

注意“render”方法发生在“render phase”,并记得整个功能组件的组件体是“render method”。这意味着 React 可以并且可能会多次渲染组件,以便计算在“提交阶段”期间需要渲染到 DOM 的内容的差异。 “提交阶段”传统上被认为是渲染组件(到 DOM)。另请注意,此阶段是可以运行副作用的地方,即useEffect

将日志和引用更新放在useEffect 挂钩中,以便每个渲染周期运行一次。

function App() {
  const [isSwapped, setIsSwapped] = React.useState(false);
  const [, dummyState] = React.useState(false);

  const renders = React.useRef(0);

  React.useEffect(() => {
    renders.current += 1;
    console.log("<App /> renders: ", renders.current);
  });

  return (
    <div className="App">
      <button onClick={() => setIsSwapped((isSwapped) => !isSwapped)}>
        Swap
      </button>
      <button onClick={() => dummyState((state) => !state)}>Re-render</button>
      {isSwapped ? (
        <>
          <First />
          <Second />
        </>
      ) : (
        <>
          <Second />
          <First />
        </>
      )}
    </div>
  );
}

演示

【讨论】:

  • 哦,好的,非常感谢您提供第二部分的回答!
  • 所以,这是我最初编写的代码,除了没有将 render.current 增量和 console.log 包装在 useEffect 中,在我看来,组件仍然在每次安装时重新渲染,这是有道理的。所以我认为理论上我可以使用useMemo 钩子来记忆组件调用,这将返回一个在多个&lt;App/&gt; 渲染之间保持不变的对象,这将解决它。你知道为什么它不起作用吗?我只是对为什么console.logs 似乎运行每个渲染感到困惑。会不会是返回的对象在 mount 时触发了 re-render 调用?
  • 哦,哇...实际上像 Ashish Tuteja 建议的那样,将 key prop 添加到记忆化的返回值中确实有效!
  • @Kirshach 是的,Ashish 提到了关于订单更改的协调过程,这会导致组件重新安装。在这些组件上使用稳定的 React 键向 React 表明它们仍然是上一个渲染周期中的相同组件。旁注,React keyref 不是传统意义上的道具,它们不会传递给孩子。
【解决方案2】:

关于您问题的第二部分,为什么 render.current 的值可能会更新两次而不是一次,这可能是因为您在 index.jsx 文件中使用了 React.StrictMode。删除 StrictMode 似乎确实解决了这个 example 中的问题。另请查看此链接以获取更多信息info

至于第一个问题,似乎在单击重新渲染按钮时,子组件(第一个或第二个)都不会重新渲染。但是,当我们交换它们时,它们不仅会重新渲染,而且组件也会被卸载然后重新安装。您也可以在上面的示例中看到这种行为。为这两个组件提供唯一键似乎可以解决此问题。我也是 React 的初学者,并不完全确定幕后发生了什么,但这是我迄今为止根据 documentation 收集到的内容:当 react 发现一个孩子(比如第一个孩子)有不同的type 与之前渲染上的类型(在我们的例子中,类型在 First 和 Second 之间切换)相比,它卸载子节点及其所有子节点,然后重新安装它们,然后继续渲染过程。我们可以在上面的示例中看到控制台日志支持这一点。通过包含 unqiue 键,我们让 react 知道尽管组件 First 和 Second 位于不同的位置,但它们毕竟是同一个组件。因此,react 不会遇到孩子交换类型的情况。

【讨论】:

  • 你太棒了!非常感谢,现在我必须消化这可能如何工作以及可能导致哪些应用程序
猜你喜欢
  • 2021-07-02
  • 1970-01-01
  • 2020-06-10
  • 2019-12-31
  • 2020-08-07
  • 1970-01-01
  • 1970-01-01
  • 2022-06-16
  • 1970-01-01
相关资源
最近更新 更多