简体   繁体   English

如何重写这个简单的组件树以利用 React.memo?

[英]How to rewrite this simple component tree to take advantage of React.memo?

In the followingCode Sandbox we have a <Child /> component that either renders within a <div /> or not, depending on state.在下面的代码沙箱中,我们有一个<Child />组件,它要么在<div />呈现,要么不在,取决于状态。

import React, { useState, memo } from "react";

const Child = memo(
  ({ n }) => {
    console.log("Re-rendered child");
    return <span>Child {n}</span>;
  },
  () => true
);

export default function App() {
  const [shouldWrapChildComponent, setShouldWrapChildComponent] = useState(
    false
  );

  return (
    <div>
      <button
        onClick={() =>
          setShouldWrapChildComponent(
            (shouldWrapChildComponent) => !shouldWrapChildComponent
          )
        }
      >
        Toggle Wrapper
      </button>

      <br />

      {[0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <div>
            <Child n={n} />
          </div>
        ) : (
          <Child n={n} />
        );
      })}
    </div>
  );
}

As you can see, the <Child /> component is using React.memo to prevent re-renders.如您所见, <Child />组件使用React.memo来防止重新渲染。

However, re-renders are not prevented when the component tree changes (and this makes sense).但是,当组件树更改时不会阻止重新渲染(这是有道理的)。 If shouldWrapChildComponent is true , then <Child /> will render inside of a <div /> , and otherwise <Child /> will render where <div /> used to render.如果shouldWrapChildComponenttrue ,则<Child />将在<div />内部渲染,否则<Child />将在<div />用于渲染的地方渲染。 This is what I mean by "the component tree changes".这就是我所说的“组件树更改”的意思。

Is there a way to rewrite the components such that the component tree does not change , and therefore can take advantage of React.memo?有没有办法重写组件,使得组件树不会改变,从而可以利用 React.memo? Or, given that we'll always need conditionally wrap the <Child /> component, is there no way to have React preserve the rendered output of <Child /> ?或者,考虑到我们总是需要有条件地包装<Child />组件,有没有办法让 React 保留<Child />的渲染输出?

However, re-renders are not prevented when the component tree changes (and this makes sense).但是,当组件树更改时不会阻止重新渲染(这是有道理的)。 If shouldWrapChildComponent is true, then will render inside of a , and otherwise will render where used to render.如果 shouldWrapChildComponent 为 true,则将在 a 内部进行渲染,否则将在用于渲染的位置进行渲染。 This is what I mean by "the component tree changes"这就是我所说的“组件树更改”的意思

Let's talk about the above quotation, let's do a small change and remove the div element:再说说上面的引文,我们做个小改动,去掉div元素:

{[0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <Child n={n} />
        ) : (
          <Child n={n} />
        );
      })
}

The result is: your Child component will re-render only 6 times even after toggling with the button.结果是:即使在切换按钮后,您的Child组件也只会重新渲染 6 次。 You can put a console.log() on the App component and check the console, the toggle button will cause a re-render for your whole App but the Child component will not re-render anymore.您可以在App组件上放置一个 console.log() 并检查控制台,切换按钮将导致整个App重新渲染,但Child组件将不再重新渲染。

So, the memorization is working properly and prevents the child component from extra re-render.因此,记忆工作正常并防止子组件额外重新渲染。

But the problem appeared when you wrap your Child component with another element ( here with div ), why?但是当你用另一个元素(这里用div )包装你的Child组件时,问题就出现了,为什么?

To check this behavior, I separate the main part from the return method by creating a resultArray variable just to log the resultArray before rendering.为了检查这种行为,我通过创建一个resultArray变量来将主要部分与返回方法分开,以便在渲染之前记录resultArray

import React, { useState, memo } from "react";

const Child = memo(
  ({ n }) => {
    console.log("Re-rendered child");
    return <span>Child {n}</span>;
  },
  () => true
);

export default function App() {
  const [shouldWrapChildComponent, setShouldWrapChildComponent] = useState(
    false
  );

  const resultArray = [0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <div>
            <Child n={n} />
          </div>
        ) : (
          <Child n={n} />
        );
      })

  console.log(`When shouldWrapChildComponent is ${shouldWrapChildComponent} the result array is: `, resultArray);

  return (
    <div>
      <button
        onClick={() =>
          setShouldWrapChildComponent(
            (shouldWrapChildComponent) => !shouldWrapChildComponent
          )
        }
      >
        Toggle Wrapper
      </button>
      <br />
      {resultArray}
    </div>
  );
}

The resultArray when shouldWrapChildComponent is false :shouldWrapChildComponentfalse时的resultArray

>(6) [Object, Object, Object, Object, Object, Object]

>0: Object
  type: "div"  // pay attention here
  key: null
  ref: null
  >props: Object
     n: 0         // ----> passing n via props to Child
  _owner: FiberNode
  _store: Object
>1: Object
>2: Object
>3: Object
>4: Object
>5: Object

The resultArray when shouldWrapChildComponent is true :shouldWrapChildComponenttrue时的resultArray

>(6) [Object, Object, Object, Object, Object, Object]

>0: Object
  type: null    // pay attention here
  key: null
  ref: null
  >props: Object     // -----> passing a object instead of Child
    >children: Object
      >type: Object
        >type:   ƒ _c() {}
        >compare: f () {}
  _owner: FiberNode
  _store: Object
>1: Object
>2: Object
>3: Object
>4: Object
>5: Object

As you can see, the result is totally different, so react will trigger a re-render every time to draw the new elements in DOM.如您所见,结果完全不同,因此每次在 DOM 中绘制新元素时,react 都会触发重新渲染。

React Reconciliation Algorithm反应协调算法

When diffing two trees, React first compares the two root elements.当比较两棵树时,React 首先比较两个根元素。 The behavior is different depending on the types of the root elements.行为因根元素的类型而异。

So, as the element type gets changed, the Reconciliation algorithm flagged it and tried to re-rendered it, this re-render is like the first render on your component before the component did mount (and cause extra re-render with your Child element).因此,随着元素type的改变, Reconciliation算法会标记它并尝试重新渲染它,这种重新渲染就像组件安装之前的第一次渲染一样(并导致额外的重新渲染与您的子元素)。

More on React official documentation .更多关于 React 官方文档

As a simplest solution作为最简单的解决方案

There are many solutions to avoid this extra re-rendering, but as a simple solution you can change the main part as below:有很多解决方案可以避免这种额外的重新渲染,但作为一个简单的解决方案,您可以更改主要部分,如下所示:

{[0, 1, 2, 3, 4, 5].map((n) => {
        return shouldWrapChildComponent ? (
          <div>
            <Child n={n} />
          </div>
        ) : (
          <div style={{display: "inline"}}>
            <Child n={n} />
          </div>
        );
      })
}

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

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