如果 areEqualFunction 执行 complex/a 大量比较,使用 React.memo 会更快吗?

Is it faster to use React.memo if the areEqualFunction performs complex/a large number of comparisons?

假设我有以下代码:

import React, { memo } from 'react';

const MyComponent = ({ arrayOfStuff }) => (
  <div>
    {arrayOfStuff.map(element => (
      <p key={element.foo}>element.foo</p>
    ))}
  </div>
);

const areEqual = (prevProps, nextProps) => {
  const prevArrayOfStuff = prevProps.arrayOfStuff;
  const nextArrayOfStuff = nextProps.arrayOfStuff;
  if (prevArrayOfStuff.length !== nextArrayOfStuff.length)
      return false;

  for (let i; i < prevArrayOfStuff.length && i < nextArrayOfStuff.length; ++i) {
    if (prevArrayOfStuff[i].foo !== nextArrayOfStuff[i].foo)
        return false;
  }

  return true;
};

export default memo(MyComponent, areEqual);

假设 arrayOfStuff 非常大,可能有数百个元素。我真的节省了很多记忆组件的时间吗?我认为如果 props 相同,它会迭代所有元素而不考虑备忘录,因为 areEqual 和渲染函数都是这样做的。

对此的最佳答案是:配置文件并查看。 :-)

但是,尽管您的数组中可能有数百个条目,但您所做的检查并不复杂,而且非常简单快捷。 (我会在开头添加一个 if (prevArrayOfStuff === nextArrayOfStuff) { return true; }。)

一些优缺点:

优点:

  • 您的检查非常简单快速,即使是数百个元素也是如此。

  • 如果没有发现任何变化,您保存:

    • 创建一堆 objects(由组件 return 编辑的 React“元素”)。
    • React 必须将先前元素的键与新元素进行比较,以查看是否需要更新 DOM。
  • 请记住,您的组件将在其 parent 发生任何更改时被调用到 re-render,即使这些更改没有'与您的组件相关。

缺点:

  • 如果数组中 经常变化,那么您只是在无回报地增加更多工作,因为 areEqual 将要 return false 无论如何。

  • areEqual 有持续的维护成本,并且它提供了出现错误的机会。

因此,这实际上归结为您的整个应用程序发生了哪些变化,尤其是组件的 parents。如果那些 parent 的状态或道具经常变化但与您的组件无关,那么您的组件进行检查可以节省很多时间。

这里展示了当 parent 中的某些内容发生更改时,即使其 props 中没有任何内容发生更改,您的组件将如何被调用到 re-render:

没有 记忆它(如果没有任何变化,React 实际上不会更新 DOM 元素,但是你的函数被调用并创建 React 比较的 React 元素渲染的):

const {useState, useEffect} = React;

// A stand-in for your component
const Example = ({items}) => {
    console.log("Example rendering");
    return <div>
        {items.map(item => <span key={item}>{item}</span>)}
    </div>;
};

// Some other component
const Other = ({counter}) => {
    console.log("Other rendering");
    return <div>{counter}</div>;
};

// A parent component
const App = () => {
    // This changes every tick of our interval timer
    const [counter, setCounter] = useState(0);
    // This changes only every three ticks
    const [items, setItems] = useState([1, 2, 3]);
    
    useEffect(() => {
        const timer = setInterval(() => {
            setCounter(c => {
                c = c + 1;
                if (c % 3 === 0) {
                    // Third tick, change `items`
                    setItems(items => [...items, items.length + 1]);
                }
                // Stop after 6 ticks
                if (c === 6) {
                    setTimeout(() => {
                        console.log("Done");
                    }, 0);
                    clearInterval(timer);
                }
                return c;
            });
        }, 500);
        return () => clearInterval(timer);
    }, []);
    
    return <div>
        <Example items={items} />
        <Other counter={counter} />
    </div>;
};

ReactDOM.render(<App/>, document.getElementById("root"));
.as-console-wrapper {
    max-height: 80% !important;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>

记忆它:

const {useState, useEffect} = React;

// A stand-in for your component
const Example = ({items}) => {
    console.log("Example rendering");
    return <div>
        {items.map(item => <span key={item}>{item}</span>)}
    </div>;
};

const examplePropsAreEqual = ({items: prevItems}, {items: nextItems}) => {
    const areEqual = (
        prevItems === nextItems ||
        (
            prevItems.length === nextItems.length &&
            prevItems.every((item, index) => item === nextItems[index])
        )
     );
     if (areEqual) {
         console.log("(skipped Example)");
     }
     return areEqual;
}

const ExampleMemoized = React.memo(Example, examplePropsAreEqual);

// Some other component
const Other = ({counter}) => {
    console.log("Other rendering");
    return <div>{counter}</div>;
};

// A parent component
const App = () => {
    // This changes every tick of our interval timer
    const [counter, setCounter] = useState(0);
    // This changes only every three ticks
    const [items, setItems] = useState([1, 2, 3]);
    
    useEffect(() => {
        const timer = setInterval(() => {
            setCounter(c => {
                c = c + 1;
                if (c % 3 === 0) {
                    // Third tick, change `items`
                    setItems(items => [...items, items.length + 1]);
                }
                // Stop after 6 ticks
                if (c === 6) {
                    setTimeout(() => {
                        console.log("Done");
                    }, 0);
                    clearInterval(timer);
                }
                return c;
            });
        }, 500);
        return () => clearInterval(timer);
    }, []);
    
    return <div>
        <ExampleMemoized items={items} />
        <Other counter={counter} />
    </div>;
};

ReactDOM.render(<App/>, document.getElementById("root"));
.as-console-wrapper {
    max-height: 80% !important;
}
<div id="root"></div>

<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>