likes
comments
collection
share

在 React 中实现记忆以提高性能小技巧

作者站长头像
站长
· 阅读数 3

React 如何渲染 UI

在详细了解 React 中的 memoization 之前,让我们先看看 React 如何使用虚拟 DOM 呈现 UI。

常规 DOM 基本上包含一组表示为树的节点。DOM 中的每个节点都是 UI 元素的表示。每当您的应用程序中发生状态更改时,该 UI 元素的相应节点及其所有的节点都会在 DOM 中文更新,然后重新绘制 UI 以反映更新后的更改。

在高效的树算法的帮助下更新节点更快,但是当 DOM 具有大量 UI 元素时,重新绘制很慢并且可能会影响性能。因此,React 中引入了虚拟 DOM。

这是真实 DOM 的虚拟表示。现在,每当应用程序的状态发生任何变化时,React 都会创建一个新的虚拟 DOM,而不是直接更新真实 DOM。React 然后将这个新的虚拟 DOM 与之前创建的虚拟 DOM 进行比较,以找出需要重新绘制的差异。

使用这些差异,虚拟 DOM 可以通过更改有效地更新真实 DOM。这提高了性能,因为虚拟 DOM 不会简单地更新 UI 元素及其所有子元素,而是仅有效地更新真实 DOM 中必要且最小的更改。

为什么我们需要 React 中的记忆

在上一节中,我们看到了 React 如何使用虚拟 DOM 有效地执行 DOM 更新以提高性能。在本节中,我们将查看一个用例,该用例解释了记忆化以进一步提高性能的必要性。

我们将创建一个父类,其中包含一个按钮来增加一个名为 的状态变量count。父组件也有一个对子组件的调用,将一个 prop 传递给它。我们还在console.log()render 两个类的方法中添加了语句:

//Parent.js
class Parent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
  }

  handleClick = () => {
    this.setState((prevState) => {
      return { count: prevState.count + 1 };
    });
  };

  render() {
    console.log("Parent render");
    return (
      <div className="App">
        <button onClick={this.handleClick}>Increment</button>
        <h2>{this.state.count}</h2>
        <Child name={"joe"} />
      </div>
    );
  }
}

export default Parent;

此示例的完整代码可在CodeSandbox上找到。

我们将创建一个Child接受父组件传递的道具并将其显示在 UI 中的类:

//Child.js
class Child extends React.Component {
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;

每当我们单击父组件中的按钮时,计数值都会发生变化。由于这是状态变化,因此调用了父组件的 render 方法。

每次父重新渲染时,传递给子类的道具保持不变,因此子组件不应重新渲染。然而,当我们运行上述代码并不断增加计数时,我们会得到以下输出:

Parent render
Child render
Parent render
Child render
Parent render
Child render

您可以在以下沙箱中自己增加上述示例的计数,并查看控制台的输出:

\

从这个输出中,我们可以看到,当父组件重新渲染时,它也会重新渲染子组件——即使传递给子组件的道具没有改变。这将导致孩子的虚拟 DOM 与之前的虚拟 DOM 进行差异检查。由于我们在子组件中没有区别——因为所有重新渲染的 props 都是相同的——所以真正的 DOM 没有更新。 我们确实有一个性能优势,即不会不必要地更新真实 DOM,但我们可以在这里看到,即使子组件中没有实际更改,也会创建新的虚拟 DOM 并执行差异检查。对于小型 React 组件,这种性能可以忽略不计,但对于大型组件,性能影响是显着的。为了避免这种重新渲染和虚拟 DOM 检查,我们使用了 memoization。

React 中的记忆

在 React 应用程序的上下文中,memoization 是一种技术,当父组件重新渲染时,子组件仅在 props 发生变化时重新渲染。如果 props 没有变化,则不会执行 render 方法,会返回缓存的结果。由于未执行 render 方法,因此不会有虚拟 DOM 创建和差异检查——从而给我们带来了性能提升。

现在,让我们看看如何在类和函数式 React 组件中实现 memoization,以避免这种不必要的重新渲染。

在类组件中实现记忆

为了在类组件中实现记忆,我们将使用React.PureComponent。React.PureComponent实现shouldComponentUpdate(),它对 state 和 props 进行浅层比较,并仅在 props 或 state 发生变化时渲染 React 组件。

将子组件更改为如下所示的代码:

//Child.js
class Child extends React.PureComponent { // Here we change React.Component to React.PureComponent
  render() {
    console.log("Child render");
    return (
      <div>
        <h2>{this.props.name}</h2>
      </div>
    );
  }
}

export default Child;

此示例的完整代码显示在以下沙箱中:

\

父组件保持不变。现在,当我们增加父组件中的计数时,控制台中的输出如下:

Parent render
Child render
Parent render
Parent render

对于第一次渲染,它调用父组件和子组件的渲染方法。

对于每次增量的后续重新渲染,仅render调用父组件的函数。子组件不会重新渲染。

在功能组件中实现记忆

为了在功能性 React 组件中实现 memoization,我们将使用React.memo()。React.memo()是一个高阶组件(HOC),它与 ​执行类似的工作PureComponent,避免不必要的重新渲染。

以下是功能组件的代码:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child); // Here we add HOC to the child component for memoization

我们还将父组件转换为功能组件,如下所示:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  console.log("Parent render");
  return (
    <div>
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} />
    </div>
  );
}

此示例的完整代码可以在以下沙箱中看到:

\

现在,当我们增加父组件中的计数时,控制台会输出以下内容:

Parent render
Child render
Parent render
Parent render
Parent render

函数道具的 React.memo() 问题

在上面的示例中,我们看到当我们React.memo()为子组件使用 HOC 时,即使父组件重新渲染,子组件也不会重新渲染。

但是,需要注意的一个小警告是,如果我们将函数作为 prop 传递给子组件,即使在 using 之后React.memo(),子组件也会重新渲染。让我们看一个例子。

我们将更改父组件,如下所示。在这里,我们添加了一个处理函数,我们将作为 props 传递给子组件:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = () => {
    console.log("handler");    // This is the new handler that will be passed to the child
  };

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}

的组件代码保持原样。我们不使用我们在子组件中作为 props 传递的函数:

//Child.js
export function Child(props) {
  console.log("Child render");
  return (
    <div>
      <h2>{props.name}</h2>
    </div>
  );
}

export default React.memo(Child);

现在,当我们增加父组件中的计数时,它会重新渲染并重新渲染子组件,即使传递的道具没有变化。

那么,是什么导致孩子重新渲染呢?答案是,每次父组件重新渲染时,都会创建一个新的处理函数并将其传递给子组件。现在,由于每次重新渲染都会重新创建处理函数,因此子组件在对 props 进行浅比较时发现处理程序引用已更改并重新渲染子组件。

在下一节中,我们将了解如何解决此问题。

useCallback()避免进一步重新渲染

导致子级重新渲染的主要问题是处理函数的重新创建,它改变了传递给子级的引用。所以,我们需要有办法避免这种娱乐。如果未重新创建处理程序,则对处理程序的引用不会改变——因此子进程不会重新渲染。

为了避免每次渲染父组件时都重新创建函数,我们将使用一个名为useCallback()的 React 钩子。Hooks 是在 React 16 中引入的。要了解有关 Hooks 的更多信息,您可以查看 React 的官方Hooks文档,或查看“ React Hooks: How to Get Started & Build Your Own ”。

钩子有useCallback()两个参数:回调函数和依赖项列表。

考虑以下useCallback() 示例:

const handleClick = useCallback(() => {
  //Do something
}, [x,y]);

在这里,useCallback()被添加到handleClick()函数中。第二个参数[x,y]可以是空数组、单个依赖项或依赖项列表。每当第二个参数中提到的任何依赖项发生变化时,才会handleClick()重新创建函数。

如果中提到的依赖useCallback()项没有改变,则返回作为第一个参数提到的回调的记忆版本。我们将更改我们的父功能组件以使用useCallback()传递给子组件的处理程序的钩子:

//Parent.js
export default function Parent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };

  const handler = useCallback(() => { //using useCallback() for the handler function
    console.log("handler");
  }, []);

  console.log("Parent render");
  return (
    <div className="App">
      <button onClick={handleClick}>Increment</button>
      <h2>{count}</h2>
      <Child name={"joe"} childFunc={handler} />
    </div>
  );
}

子组件代码保持原样。

此示例的完整代码如下所示:

\

当我们为上面的代码增加父组件中的计数时,我们可以看到以下输出:

Parent render
Child render
Parent render
Parent render
Parent render

由于我们为父处理程序使用了useCallback()钩子,因此每次父处理程序重新渲染时,处理程序函数都不会重新创建,并且处理程序的记忆版本被发送给子处理程序。子组件将进行浅层比较,并注意到处理函数的引用没有改变——因此它不会调用该render方法。

要记住的事情

记忆化是一种提高 React 应用程序性能的好技术,如果组件的 props 或 state 没有改变,它可以避免不必要的重新渲染组件。你可能会想到只为所有组件添加 memoization,但这不是构建 React 组件的好方法。你应该只在没有组件的情况下使用记忆:

  • 给定相同的道具时返回相同的输出
  • 有多个 UI 元素,虚拟 DOM 检查会影响性能
  • 经常提供相同的道具

\

如果本文对你有帮助,别忘记给我个3连 ,点赞,转发,评论,,咱们下期见。

收藏 等于白嫖,点赞才是真情。

亲爱的小伙伴们,有需要JAVA面试文档资料请点赞+转发,关注我后,私信我333就可以领取免费资料哦