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

Posted

技术标签:

【中文标题】为啥 JSX 道具不应该使用箭头函数或绑定?【英文标题】:Why shouldn't JSX props use arrow functions or bind?为什么 JSX 道具不应该使用箭头函数或绑定? 【发布时间】:2017-05-25 07:36:10 【问题描述】:

我正在使用我的 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 中使用箭头函数或绑定是一种不好的做法,会损害性能,因为函数会在每次渲染时重新创建。

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

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

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

示例 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的绑定方法

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

    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>
        );
      
    
    

    使用带有箭头函数的proposal-class-fields 绑定方法。由于这是第 3 阶段的提案,您需要将 Stage 3 preset 或 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>

【讨论】:

如何在无状态组件上实现这一点? 无状态(函数)组件没有this,所以没有什么可以绑定的。通常这些方法由包装智能组件提供。 @OriDrori:当您需要在回调中传递数据时,它是如何工作的? onClick=() =&gt; onTodoClick(todo.id) @adam-beck - 将其添加到类 cb() onTodoClick(this.props.todo.id); 的回调方法定义中。 @adam-beck 我认为这是如何将useCallback 与动态值一起使用。 ***.com/questions/55006061/…【参考方案2】:

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

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

【讨论】:

不仅如此。每次创建新的函数实例意味着状态被修改,当组件的状态被修改时,它将被重新渲染。由于使用 React 的主要原因之一是只渲染发生变化的元素,因此在这里使用 bind 或箭头函数是自找麻烦。但是,它没有有很好的记录,尤其是在使用列表中的mapping 数组等情况下。 “每次创建新的函数实例意味着状态被修改”你是什么意思?问题中根本没有状态【参考方案3】:

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

这条规则是从箭头函数不那么普遍并且人们使用 .bind(this) 的时候开始的,它曾经很慢。 Chrome 49 中已修复性能问题。

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

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

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

【讨论】:

您能否展示如何使用内联箭头函数对组件编写单元测试? ^^ @krankuba 这不是这个问题的目的。您仍然可以传入未内联定义但仍不可测试的匿名函数。【参考方案4】:

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

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

传统上,围绕 React 中的内联函数的性能问题与在每次渲染时传递新回调如何破坏子组件中的 shouldComponentUpdate 优化有关。 (docs)

这不是额外的函数创建成本:

Function.prototype.bindgot fixed here 和箭头函数的性能问题要么是原生的,要么是被 babel 转译为普通函数;在这两种情况下,我们都可以假设它并不慢。 (React Training)

我相信那些声称创建函数很昂贵的人总是被误导(React 团队从未这样说过)。 (Tweet)

react/jsx-no-bind 规则什么时候有用?

您要确保记忆化的组件按预期工作:

React.memo(功能组件) PureComponent 或自定义 shouldComponentUpdate(用于类组件)

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

如何解决 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

【讨论】:

【参考方案5】:

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

【讨论】:

【参考方案6】:

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

注意:在内部它通过指定的键缓存你的箭头函数, 无需担心重新渲染!

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

其他功能:

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

【讨论】:

问题是我们为什么不能使用它。不是如何将它与其他黑客一起使用。

以上是关于为啥 JSX 道具不应该使用箭头函数或绑定?的主要内容,如果未能解决你的问题,请参考以下文章

为啥我能够使用 this.state 而无需绑定或使用箭头函数 React

[转]为啥在 React 的 Render 中使用箭头函数和 bind 会造成问题

在react jsx中,为什么使用箭头函数和bind容易出现问题

如何在带有 React 的 Typescript/JSX 中使用带有箭头函数的泛型?

箭头函数

微信小程序--箭头表达式