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

Why shouldn't JSX props use arrow functions or bind?

我正在 运行 与我的 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>
))}

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

为什么你不应该在 JSX 属性中使用内联箭头函数

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

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

  2. 使用内联箭头函数将导致 PureComponents,并且在 shouldComponentUpdate 方法中使用 shallowCompare 的组件无论如何都会重新渲染。由于每次都重新创建箭头函数 prop,浅比较会将其识别为对 prop 的更改,并且组件将重新渲染。

正如您在以下 2 个示例中看到的那样 - 当我们使用内联箭头函数时,<Button> 组件每次都会重新呈现(控制台显示 'render 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 - PureComponent with 内联处理程序

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>

在不内联箭头函数的情况下将方法绑定到 this

  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. 正在使用 proposal-class-fields with an arrow function. As this is a stage 3 proposal, you'll need to add the Stage 3 preset or the Class properties transform 将方法绑定到您的 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>
        );
      }
    }
    

具有内部回调的函数组件

当我们在函数组件中创建内部函数(例如事件处理程序)时,每次渲染组件时都会重新创建该函数。如果该函数作为道具(或通过上下文)传递给子组件(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() hook 包装回调,并将依赖项设置为空数组。

注意: useState 生成的函数接受一个提供当前状态的更新函数。这样我们就不需要给当前状态设置一个useCallback.

的依赖

示例 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>

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

您可以在 https://github.com/yannickcr/eslint-plugin-react/blob/master/docs/rules/jsx-no-bind.md

查看完整的解释和更多信息

为避免创建具有相同参数的新函数,您可以记住函数绑定结果,这里有一个名为 memobind 的简单实用程序可以执行此操作:https://github.com/supnate/memobind

像这样使用内联函数非常好。 linting 规则已过时。

这个规则来自于箭头函数不那么普遍并且人们使用 .bind(this) 的时代,它曾经很慢。性能问题已在 Chrome 49.

中修复

请注意不要将内联函数作为 props 传递给子组件。

React Router 的作者 Ryan Florence 对此写了一篇很棒的文章:

https://cdb.reacttraining.com/react-inline-functions-and-performance-bdff784f5578

您可以使用 react-cached-handler 库使用箭头函数,无需担心重新渲染性能:

Note : Internally it caches your arrow functions by the specified key, no need to be worried about re-rendering!

render() {
    return (
        <div>
            {this.props.photos.map((photo) => (
                <Photo
                    key={photo.url}
                    onClick={this.handler(photo.url, (url) => {
                        console.log(url);
                    })}
                />
            ))}
        </div>
    );
}

其他功能:

  • 命名处理程序
  • 通过箭头函数处理事件
  • 访问密钥、自定义参数和原始事件
  • 组件渲染性能
  • 处理程序的自定义上下文

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

主要是因为内联函数会破坏优化组件的记忆:

Traditionally, performance concerns around inline functions in React have been related to how passing new callbacks on each render breaks shouldComponentUpdate optimizations in child components. (docs)

额外函数创建成本较少:

Performance issues with Function.prototype.bind got fixed here and arrow functions are either a native thing or are transpiled by babel to plain functions; in both cases we can assume it’s not slow. (React Training)

I believe people claiming function creation is expensive have always been misinformed (React team never said this). (Tweet)

react/jsx-no-bind 规则何时有用?

您想确保记忆组件按预期工作:

  • React.memo(对于函数组件)
  • PureComponent 或自定义 shouldComponentUpdate(对于 class 组件)

通过遵守此规则,传递了稳定的函数对象引用。因此,当先前的道具没有改变时,上述组件可以通过防止重新渲染来优化性能。

如何解决ESLint错误?

类:将处理程序定义为方法,或 class property 用于 this 绑定。
挂钩:使用 useCallback.

中间地带

在很多情况下,内联函数使用起来非常方便,对性能的要求也绝对没问题。不幸的是,这条规则不能仅限于记忆组件类型。如果您仍然想全面使用它,您可以例如disable it 对于简单 DOM 节点:

rules: {
  "react/jsx-no-bind": [ "error", { "ignoreDOMComponents": true } ],
}

const Comp = () => <span onClick={() => console.log("Hello!")} />; // no warning