React hooks中函数组件内的函数 - 性能

37

需要在React Hooks 的函数组件中编写函数的建议。

就我所研究的而言,许多人都认为这是不良实践, 因为每次调用重新渲染时都会创建嵌套/内部函数。 经过一些分析后,

我发现我们可以在元素上使用onClick={handleClick.bind(null, props)}并将函数放置在函数组件外部。

例:

const HelloWorld = () => {
  function handleClick = (event) => {
    console.log(event.target.value);
  }

  return() {
    <>
        <input type="text" onChange={handleClick}/>
    </>
  }
}
请告知是否有任何其他方式。
提前致谢。

2
我不清楚你的示例展示了什么。无论如何,你每次渲染都会创建一个新函数。此外,示例函数似乎是错误的。 - Dave Newton
在函数组件中,我们如何避免每次渲染都创建新的函数? - Johnson Samuel
1
useCallback - Arman Charan
如果函数或其依赖项未更改,则useCallback将(大致)不执行任何操作。 - Dave Newton
8个回答

38

不用担心

不要担心在每次渲染时创建新的函数。只有在极端情况下,这才会影响性能。 设置onClick处理程序不是其中之一,因此只需在每次渲染时创建一个新函数即可。

但是,当您需要确保每次使用相同的函数时,可以使用useCallback

为什么不使用useCallback来处理onClick

以下是为什么不必费心使用useCallback处理onClick处理程序(以及大多数其他事件处理程序)的原因。

考虑以下代码片段,一个没有使用useCallback:

function Comp(props) {
  return <button onClick={() => console.log("clicked", props.foo)}>Text</Button>
}

还有一个使用useCallback的:

function Comp(props) {
  const onClick = useCallback(() => {
    console.log("clicked", props.foo)
  }, [props.foo])

  return <button onClick={onClick}>Text</Button>
}

在后一种情况下,唯一的区别是如果props.foo保持不变,React不必更改您按钮上的onClick。更改回调非常便宜,完全没有值得为了理论上的性能提升而使代码复杂化。
另外,值得注意的是,即使使用useCallback,每次呈现仍然会创建一个新函数,但只要作为第二个参数传递的依赖项保持不变,useCallback将返回旧函数。
为什么要使用useCallback 使用useCallback的目的在于,如果使用引用相等比较两个函数,则fn === fn2仅在fnfn2指向内存中的同一函数时才为真。无论这些函数是否执行相同的操作都没有关系。
因此,如果您有记忆化或仅在函数更改时运行代码的其他方式,则使用useCallback重用相同的函数可能很有用。
例如,React钩子比较旧的和新的依赖关系,probably使用Object.is

另一个例子是React.PureComponent,它只会在props或state发生更改时重新渲染。这对于使用大量资源进行渲染的组件非常有用。每次渲染都向PureComponent传递一个新的onClick将导致它每次重新渲染。


8
值得注意的是,即使使用 useCallback,每次渲染时仍会创建一个新的函数。+1 - gaurav5430
1
可能是这样,虽然我现在找不到了,@chetan。用我的话来解释:要将函数传递给useCallback,必须先创建它。这会在每次渲染时发生。React会跟踪调用useCallback(或任何钩子)的组件实例,并根据依赖项返回正确的(旧的或新的)函数。 - ArneHugo
是的,我确定。函数组件中的代码在每次调用时运行(当它被渲染时会发生这种情况),包括创建传递给useCallback的函数的代码。至少目前来说,在JavaScript语言中没有办法避免这种情况。(您链接的文章确实存在误导性,因为它陈述了“函数不会在组件的每次重新渲染时重新初始化”的说法。它们确实会被重新创建,但是会进行记忆化处理,因此您将获得先前的函数,除非依赖项已更改。) - ArneHugo
此外,值得注意的是,即使您使用 useCallback,每次渲染仍会创建一个新函数,这是不正确的。React在幕后使用索引来标记每个钩子调用(这就是为什么存在钩子规则)。因此,只要依赖项数组相同,就会返回相同的函数,因此不会创建新函数。 - pouria
1
@pouria,除非依赖项发生更改,否则相同的函数将被返回,但在每次渲染时仍会创建一个新函数。(新函数被传递到useCallback中,而useCallback决定返回新函数还是旧函数,就这样。React无法插入代码以阻止未使用的函数在第一次创建时被创建。) - ArneHugo
显示剩余4条评论

14

很多人认为这是一种不好的做法,因为它在每次重新渲染时都会创建嵌套/内部函数。

不,内部函数 / 闭包很常见,它们没有任何问题。引擎可以对它们进行大量优化。

这里的重点是,将函数作为属性传递给子组件。由于该函数被“重新创建”,它不等同于之前传递的函数,因此子组件会重新渲染(这是性能差的原因)。

你可以使用 useCallback 来解决这个问题,它会记住函数的引用。


10

enter image description here

这是一个有趣的问题,我和我的同事们对此有些担忧,所以我进行了一次测试。

我创建了一个使用Hooks的组件和一个使用Class的组件,放置了一些函数,然后将其渲染1000次。

Class组件如下:

export class ComponentClass extends React.PureComponent {
  click1 = () => {
    return console.log("just a log");
  };

  render() {
    return (
      <>
        <span onClick={this.click1}>1</span>
      </>
    );
  }
}

组件使用Hooks的形式如下:
export const ComponentHook = React.memo((props) => {
  const click1 = () => {
    return console.log("just a log");
  };

  return (
    <>
      <span onClick={click1}>1</span>
    </>
  );
});

我已经为组件添加了更多的点击处理程序,然后将它们渲染了1000次。由于Class不会在每次渲染时定义函数,所以它更快。如果你增加定义的函数数量,那么差异就会更大。
这里有一个codesandbox,你可以测试Class和Hooks的性能:https://codesandbox.io/s/hooks-vs-class-forked-erdpb

1
这里,使用 useCallback 修复了你的代码 ;) https://codesandbox.io/s/hooks-vs-class-with-usecallback-fuyrx2?file=/src/ComponentHook.js 现在使用 hooks 的版本更快了。 - justdvl

8

useCallback

useCallback 是一个有用的功能:

const HelloWorld = ({ dispatch }) => {
  const handleClick = useCallback((event) => {
    dispatch(() => {console.log(event.target.value)});
  })

  return() {
    <>
        <input type="name" onChange={handleClick}/>
    </>
  }
}

useCallback 会返回一个记忆化的回调函数版本,只有在依赖项更改时才会更改。当将回调传递给依赖于引用相等性以防止不必要渲染(例如 shouldComponentUpdate)的优化子组件时,这非常有用。

有关详细信息,请访问React文档参考:React useCallback


旧解决方案

首选解决方案: 将您的 handleClick 函数传递给您的函数组件。

const HelloWorld = (props) => {

  return() {
    <>
        <input type="name" onChange={props.handleClick}/>
    </>
  }
}

第二种解决方案: 将您的函数定义在功能组件之外。

3

0
根据React文档(结尾部分)所述,

后一种语法的问题在于每次LoggingButton渲染时都会创建一个不同的回调函数。在大多数情况下,这是可以接受的。但是,如果将此回调函数作为属性传递给较低级别的组件,则这些组件可能会进行额外的重新渲染。我们通常建议在构造函数中绑定或使用类字段语法,以避免出现此类性能问题。

类字段语法:

class LoggingButton extends React.Component {
  // This syntax ensures `this` is bound within handleClick.
  // Warning: this is *experimental* syntax.
  handleClick = () => {
    console.log('this is:', this);
  }

  render() {
    return (
      <button onClick={this.handleClick}>
        Click me
      </button>
    );
  }
}

回调语法中的箭头函数:

class LoggingButton extends React.Component {
  handleClick() {
    console.log('this is:', this);
  }

  render() {
    // This syntax ensures `this` is bound within handleClick
    return (
      <button onClick={() => this.handleClick()}>
        Click me
      </button>
    );
  }
}

0

只需使用useCallback

为什么需要在组件内部定义函数而不是其他地方呢?因为你要么必须将它传递给另一个子组件,要么必须在effect、memo或另一个回调中使用它。对于这些情况中的任何一种,如果你不将函数包装在useCallback中,就会传递一个新的函数,并导致组件重新渲染,memo重新运行,effect重新运行,或者回调重新定义。

你永远无法避免重新定义函数本身所带来的性能损耗,但你可以避免执行任何以该函数作为依赖项的计算,以确定是否需要运行(无论是组件还是钩子)所带来的性能损耗。

所以...只需将组件中的每个函数都包装在useCallback中,然后忘记它,从未见过任何情况会造成任何伤害。如果可以在组件外部定义函数,那总是更好的选择。


0

在这些情况下,我会老实地使用类组件。我知道过早优化的问题,但每次创建一个新函数似乎只是一种浪费,而且没有太多可维护性的好处。tibbus已经展示了性能问题,内联函数可能比类方法难以阅读。你失去的只是编写功能组件时的流畅感。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接