35

我目前对新的 react hooks API 的用例以及你可以用它做什么感到非常惊讶。

实验时出现的一个问题是,总是创建一个新的处理函数只是为了在使用useCallback.

考虑这个例子:

const MyCounter = ({initial}) => {
    const [count, setCount] = useState(initial);

    const increase = useCallback(() => setCount(count => count + 1), [setCount]);
    const decrease = useCallback(() => setCount(count => count > 0 ? count - 1 : 0), [setCount]);

    return (
        <div className="counter">
            <p>The count is {count}.</p>
            <button onClick={decrease} disabled={count === 0}> - </button>
            <button onClick={increase}> + </button>
        </div>
    );
};

尽管我将处理程序包装到 auseCallback中以避免每次它呈现内联箭头函数时传递一个新的处理程序,但仍然必须创建只是在大多数情况下被丢弃。

如果我只渲染几个组件,可能没什么大不了的。但是,如果我这样做 1000 次,对性能的影响有多大?是否存在明显的性能损失?什么是避免它的方法?可能是一个静态处理程序工厂,仅在必须创建新处理程序时才被调用?

4

5 回答 5

16

React FAQs提供了一个解释

由于在渲染中创建函数,Hooks 会变慢吗?

不会。在现代浏览器中,闭包与类相比的原始性能没有显着差异,除非在极端情况下。

此外,考虑到 Hooks 的设计在以下几个方面更有效:

Hooks 避免了类所需的大量开销,例如在构造函数中创建类实例和绑定事件处理程序的成本。

使用 Hooks 的惯用代码不需要在使用高阶组件、渲染道具和上下文的代码库中普遍存在的深层组件树嵌套。使用更小的组件树,React 要做的工作更少。

传统上,围绕 React 中内联函数的性能问题与如何在每次渲染时传递新的回调中断子组件中的 shouldComponentUpdate 优化有关。Hooks 从三个方面解决了这个问题。

所以钩子提供的整体好处远大于创建新函数的代价

此外,对于功能组件,您可以通过使用进行优化,useMemo以便在组件的道具没有变化时重新渲染组件。

于 2018-11-16T10:34:09.613 回答
11

我用下面的例子做了一个简单的测试,它使用 10k(和 100k)usingCallback个钩子并每 100 毫秒重新渲染一次。似乎数量useCallback确实很多时可能会产生影响。请参阅下面的结果。

带有 10k 个钩子的函数组件:

在此处输入图像描述

每次渲染耗时 8~12ms。

带有 100k 钩子的函数组件:

在此处输入图像描述

每次渲染耗时 25~80ms。

具有 10k 个方法的类组件:

在此处输入图像描述

每次渲染耗时 4~5ms。

具有 100k 方法的类组件: 在此处输入图像描述

每次渲染耗时 4~6ms。

我也用 1k 的例子进行了测试。但是配置文件结果看起来与 10k 的配置文件几乎相同。

因此,当我的组件使用 100k 挂钩而类组件没有显示出明显的差异时,我的浏览器中的惩罚很明显。所以我想只要你没有使用超过 10k 钩子的组件就应该没问题。该数字可能取决于客户端的运行时资源。

测试组件代码:

import React, { useState, useCallback, useEffect } from 'react';

const callbackCount = 10000
const useCrazyCounter = () => {
  const callbacks = []
  const [count, setCount] = useState(0)
  for (let i = 1; i < callbackCount + 1; i++) {
    // eslint-disable-next-line
    callbacks.push(useCallback(() => {
      setCount(prev => prev + i)
      // eslint-disable-next-line
    }, []))
  }
  return [count, ...callbacks]
}

const Counter = () => {
  const [count, plusOne] = useCrazyCounter()
  useEffect(() => {
    const timer = setInterval(plusOne, 100)
    return () => {
      clearInterval(timer)
    }}
  , [])
  return <div><div>{count}</div><div><button onClick={plusOne}>Plus One</button></div></div>
}

class ClassCounter extends React.Component {
  constructor() {
    super()
    this.state = {
      count: 0
    }
    for (let i = 1; i < callbackCount; i++) {
      this['plus'+i] = () => {
        this.setState(prev => ({
          count: prev.count + i
        }))
      }
    }
  }

  componentDidMount() {
    this.timer = setInterval(() => {
      this.plus1()
    }, 100)
  }

  componentWillUnmount() {
    clearInterval(this.timer)
  }

  render () {
    return <div><div>{this.state.count}</div><div><button onClick={this.plus1}>Plus One</button></div></div>
  }

}

const App = () => {

  return (
    <div className="App">
      <Counter/>
      {/* <ClassCounter/> */}
    </div>
  );
}

export default App;
于 2020-03-21T16:05:56.083 回答
5

但是,如果我这样做 1000 次,对性能的影响有多大?是否存在明显的性能损失?

这取决于应用程序。如果您只是简单地渲染 1000 行计数器,那可能没问题,如下面的代码片段所示。请注意,如果您只是修改个人的状态<Counter />,则仅重新渲染该计数器,其他 999 个计数器不受影响。

但我认为你担心这里无关紧要的事情。在现实世界的应用程序中,不太可能呈现 1000 个列表元素。如果您的应用程序必须呈现 1000 个项目,那么您设计应用程序的方式可能有问题。

  1. 您不应该在 DOM 中呈现 1000 个项目。无论有没有现代 JavaScript 框架,从性能和用户体验的角度来看,这通常都很糟糕。您可以使用窗口技​​术并仅渲染您在屏幕上看到的项目,其他屏幕外项目可以在内存中。

  2. 实施shouldComponentUpdate(或useMemo)以便在顶级组件必须重新渲染时不会重新渲染其他项目。

  3. 通过使用函数,你可以避免类的开销和其他一些你不知道的类相关的东西,因为 React 会自动为你做。由于在函数中调用了一些钩子,您会损失一些性能,但您也会在其他地方获得一些性能。

  4. 最后,请注意您正在调用useXXX钩子,而不是执行传递给钩子的回调函数。我确信 React 团队在使钩子调用轻量级调用钩子不应该太昂贵方面做得很好。

什么是避免它的方法?

我怀疑是否会有一个真实的场景,你需要创建一千次有状态的项目。但如果你真的需要,最好将状态提升到父组件中,并将值和递增/递减回调作为道具传递给每个项目。这样,您的各个项目不必创建状态修饰符回调,并且可以简单地使用其父项的回调属性。此外,无状态子组件更容易实现各种众所周知的性能优化。

最后,我想重申你不应该担心这个问题,因为你应该尽量避免让自己陷入这种情况而不是处理它,利用窗口和分页等技术 - 只加载你的数据需要显示在当前页面。

const Counter = ({ initial }) => {
  const [count, setCount] = React.useState(initial);

  const increase = React.useCallback(() => setCount(count => count + 1), [setCount]);
  const decrease = React.useCallback(
    () => setCount(count => (count > 0 ? count - 1 : 0)),
    [setCount]
  );

  return (
    <div className="counter">
      <p>The count is {count}.</p>
      <button onClick={decrease} disabled={count === 0}>
        -
      </button>
      <button onClick={increase}>+</button>
    </div>
  );
};

function App() {
  const [count, setCount] = React.useState(1000);
  return (
    <div>
      <h1>Counters: {count}</h1>
      <button onClick={() => {
        setCount(count + 1);
      }}>Add Counter</button>
      <hr/>
      {(() => {
        const items = [];
        for (let i = 0; i < count; i++) {
          items.push(<Counter key={i} initial={i} />);
        }
        return items;
      })()}
    </div>
  );
}


ReactDOM.render(
  <div>
    <App />
  </div>,
  document.querySelector("#app")
);
<script src="https://unpkg.com/react@16.7.0-alpha.0/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@16.7.0-alpha.0/umd/react-dom.development.js"></script>

<div id="app"></div>

于 2018-11-19T05:46:08.813 回答
0

一种方法是记忆回调以防止不必要的子组件更新。您可以在此处
阅读有关此内容的更多信息。

另外,我创建了一个 npm 包useMemoizedCallback,我希望它可以帮助任何寻求提高性能的解决方案的人。

于 2019-10-08T11:07:22.420 回答
-2

你是对的,在大型应用程序中,这可能会导致性能问题。在将处理程序传递给组件之前绑定处理程序可以避免子组件可能会进行额外的重新渲染。

<button onClick={(e) => this.handleClick(e)}>click me!</button>
<button onClick={this.handleClick.bind(this)}>click me!</button>

两者是等价的。表示 React 事件的 e 参数,而使用箭头函数时,我们必须显式传递它,绑定任何参数都会自动转发。

于 2018-11-16T10:39:45.243 回答