为什么 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>
))}

这是一个应该避免的坏习惯吗? 最好的方法是什么?

97623 次浏览

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

在 JSX 中使用箭头函数或绑定是一种损害性能的糟糕做法,因为函数是在每次呈现时重新创建的。

  1. 无论何时创建一个函数,前一个函数都会被垃圾收集。重新呈现许多元素可能会在动画中产生 jank。

  2. 使用内联箭头函数将导致 PureComponentshouldComponentUpdate方法中使用 shallowCompare的组件重新呈现。由于每次都会重新创建箭头函数道具,因此浅表比较将把它标识为道具的更改,并且组件将重新呈现。

正如你可以在下面的两个例子中看到的-当我们使用内联箭头函数时,<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 内联处理程序

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. Binding a method using the 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 to your babel configuration.

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

Function Components with inner callbacks

When we create an inner function (event handler for example) inside a function component, the function will be recreated every time the component is rendered. If the function is passed as props (or via context) to a child component (Button in this case), that child will re-render as well.

Example 1 - Function Component with an inner callback:

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()钩子包装回调函数,并将依赖项设置为空数组。

注意: useState生成的函数接受一个 updater 函数,该函数提供当前状态。通过这种方式,我们不需要将当前状态设置为 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 属性中使用,箭头函数显然会在每次呈现时创建函数的新实例。这可能会对垃圾收集器造成巨大的压力,也会阻碍浏览器优化任何“热路径”,因为函数将被丢弃而不是重用。

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

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

像这样使用内联函数是完全可以的,但是 linting 规则已经过时了。

这条规则来自于箭头函数不常用的时代。捆绑(这个) ,过去是很慢的。Chrome 49的性能问题已经解决了。

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

《反应路由器》(React Router)一书的作者瑞安•弗洛伦斯(Ryan Florence)就此写了一篇很棒的文章:

Https://reacttraining.com/blog/react-inline-functions-and-performance/

您可以使用 反应-缓存-处理程序库中的箭头函数,无需担心重新呈现性能:

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

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

其他特点:

  • 有名字的联络人
  • 用箭头函数处理事件
  • 对密钥、自定义参数和原始事件的访问
  • 组件呈现性能
  • 处理程序的自定义上下文

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

主要是因为内联函数可以破坏优化组件的制表功能:

传统上,React 中内联函数的性能问题与在每个呈现上传递新回调如何破坏子组件中的 shouldComponentUpdate优化有关。(医生)

这与额外的职能创建成本关系不大:

Function.prototype.bind 在这里被修好了和箭头函数的性能问题要么是本机问题,要么是由 babel 转换为普通函数的问题; 在这两种情况下,我们都可以假定它并不慢。(反应训练)

我相信声称功能创建昂贵的人们总是被误导(反应小组从来没有这样说过)

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

您希望确保制表组件按预期工作:

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

通过遵守此规则,可以传递稳定的函数对象引用。因此,上面的组件可以通过防止在以前的道具没有更改时重新呈现来优化性能。

如何解决 ESLint 错误?

类: 将处理程序定义为方法,或者将 类别财产定义为 this绑定。
钩子: 使用 useCallback

Middleground

在许多情况下,内联函数使用起来非常方便,而且在性能要求方面完全没有问题。遗憾的是,此规则不能仅限于制表组件类型。如果您仍然希望全面使用它,可以将 关掉它用于简单的 DOM 节点:

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


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

如果您在 onClick 处理程序中使用的函数是在呈现方法之外定义的常规(非内联)函数,但是使用了 function关键字,那么您也可能会看到这个错误。

enter image description here

在呈现方法之外,使用 const 将您的处理程序函数声明为箭头函数,React 将停止抱怨..。

enter image description here

对于那些想知道什么时候需要在回调中传递数据的人,例如:

const list = todos.map((todo, index) => (
<Todo
onClick={() => { onTodoClick(todo.id, todo.title, index) }
/>
));

解决方案

根据官方文件 ,你应该:

  1. 将函数参数移动到子组件中:
const list = todos.map((todo, index) => (
<Todo
onClick={onTodoClick}
todoId={todo.id}
todoTitle={todo.title}
indexOnList={index}
/>
));
  1. 在子组件(<Todo />)中,在调用中传递参数:
function Todo(props) {
// component properties
const { onClick, todoId, todoTitle, indexOnList } = props;


// we move the call from the parent to the children
const handleClick = useCallback(() => {
onClick(todoId, todoTitle, indexOnList);
}, [todoId, todoTitle, indexOnList]);


return (
<div onClick={handleClick}>
{/* the rest of the component remains the same */}
</div>
);
}

这是最好的解决办法吗?

我不喜欢这个解决方案。您最终将在子组件中获得父组件的数据和逻辑。这使得子组件依赖于父组件,打破了依赖规则。

这对我来说是个大大的禁忌。

我要做的就是废除这个规则。根据 Ryan Florence (React Router 的作者)的说法,这并不是什么大不了的事情: Https://medium.com/@ryanflorence/react-inline-functions-and-performance-bdff784f5578