为什么 JSX 道具不应该使用箭头函数或绑定?

我正在使用我的 React 应用程序运行 lint,但我收到此错误:

error    JSX props should not use arrow functions        react/jsx-no-bind

这就是我运行箭头函数的地方(内部):onClick

{this.state.photos.map(tile => (
  <span key={tile.img}>
    <Checkbox
      defaultChecked={tile.checked}
      onCheck={() => this.selectPicture(tile)}
      style={{position: 'absolute', zIndex: 99, padding: 5, backgroundColor: 'rgba(255, 255, 255, 0.72)'}}
    />
    <GridTile
      title={tile.title}
      subtitle={<span>by <b>{tile.author}</b></span>}
      actionIcon={<IconButton onClick={() => this.handleDelete(tile)}><Delete color="white"/></IconButton>}
    >
      <img onClick={() => this.handleOpen(tile.img)} src={tile.img} style={{cursor: 'pointer'}}/>
    </GridTile>
  </span>
))}

这是应该避免的不良做法吗?最好的方法是什么?


答案 1

为什么不应该在 JSX 道具中使用内联箭头函数

在 JSX 中使用箭头函数或绑定是一种不良做法,会损害性能,因为该函数是在每次渲染上重新创建的。

  1. 每当创建函数时,都会对前一个函数进行垃圾回收。重新渲染许多元素可能会在动画中产生卡顿。

  2. 使用内联箭头函数将导致 s 和在该方法中使用的组件重新呈现。由于每次都会重新创建箭头函数 prop,因此浅层比较会将其标识为对 prop 的更改,并且组件将重新呈现。PureComponentshallowCompareshouldComponentUpdate

正如您在以下2个示例中看到的那样 - 当我们使用内联箭头函数时,每次都会重新呈现组件(控制台显示“渲染按钮”文本)。<Button>

示例 1 - 不带内联处理程序的 PureComponent

class Button extends React.PureComponent {
  render() {
    const { onClick } = this.props;
    
    console.log('render button');
    
    return (
      <button onClick={ onClick }>Click</button>
    );
  }
}

class Parent extends React.Component {
  state = {
    counter: 0
  }
  
  onClick = () => this.setState((prevState) => ({
    counter: prevState.counter + 1
  }));
  
  render() {
    const { counter } = this.state;
    
    return (
      <div>
        <Button onClick={ this.onClick } />
        <div>{ counter }</div>
      </div>
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

示例 2 - 具有内联处理程序的纯组件

class Button extends React.PureComponent {
  render() {
    const { onClick } = this.props;
    
    console.log('render button');
    
    return (
      <button onClick={ onClick }>Click</button>
    );
  }
}

class Parent extends React.Component {
  state = {
    counter: 0
  }
  
  render() {
    const { counter } = this.state;
    
    return (
      <div>
        <Button onClick={ () => this.setState((prevState) => ({
          counter: prevState.counter + 1
        })) } />
        <div>{ counter }</div>
      </div>
    );
  }
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.production.min.js"></script>
<div id="root"></div>

将方法绑定到函数而不内联箭头函数

  1. 在构造函数中手动绑定方法:

    class Button extends React.Component {
      constructor(props, context) {
        super(props, context);
    
        this.cb = this.cb.bind(this);
      }
    
      cb() {
    
      }
    
      render() {
        return (
          <button onClick={ this.cb }>Click</button>
        );
      }
    }
    
  2. 使用箭头函数绑定使用建议类字段的方法。由于这是第 3 阶段的建议,因此您需要将阶段 3 预设类属性转换添加到 babel 配置中。

    class Button extends React.Component {
      cb = () => { // the class property is initialized with an arrow function that binds this to the class
    
      }
    
      render() {
        return (
          <button onClick={ this.cb }>Click</button>
        );
      }
    }
    

具有内部回调的函数组件

当我们在函数组件内创建一个内部函数(例如事件处理程序)时,每次渲染组件时都会重新创建该函数。如果函数作为 props(或通过上下文)传递给子组件(在本例中),则该子组件也将重新呈现。Button

示例 1 - 具有内部回调的函数组件:

const { memo, useState } = React;

const Button = memo(({ onClick }) => console.log('render button') || (
  <button onClick={onClick}>Click</button>
));

const Parent = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = () => setCounter(counter => counter + 1); // the function is recreated all the time
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

为了解决这个问题,我们可以用 useCallback() 钩子包装回调,并将依赖项设置为空数组。

注意:生成的函数接受提供当前状态的更新程序函数。这样,我们就不需要将当前状态设置为 的依赖项。useStateuseCallback

示例 2 - 具有使用 useCallback 包装的内部回调的函数组件:

const { memo, useState, useCallback } = React;

const Button = memo(({ onClick }) => console.log('render button') || (
  <button onClick={onClick}>Click</button>
));

const Parent = () => {
  const [counter, setCounter] = useState(0);
  
  const increment = useCallback(() => setCounter(counter => counter + 1), []);
  
  return (
    <div>
      <Button onClick={increment} />
      
      <div>{counter}</div>
    </div>
  );
}

ReactDOM.render(
  <Parent />,
  document.getElementById('root')
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

<div id="root"></div>

答案 2

这是因为如果在 JSX 属性中使用,箭头函数显然会在每次渲染上创建该函数的新实例。这可能会对垃圾回收器造成巨大的压力,并且还会阻碍浏览器优化任何“热路径”,因为函数将被丢弃而不是重用。

您可以在 https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md 查看整个解释和更多信息