如何避免在渲染方法中使用绑定或内联箭头函数

37

在render方法内部应避免使用方法绑定,因为在重新渲染时会创建新的方法而不是使用旧的方法,这会影响性能。

因此,在类似以下情况的场景中:

<input onChange = { this._handleChange.bind(this) } ...../>

我们可以在构造函数中绑定_handleChange方法:

this._handleChange = this._handleChange.bind(this);

或者我们可以使用属性初始化语法

_handleChange = () => {....}

现在让我们考虑一种情况,即当我们想要传递一些额外的参数时,比如在一个简单的待办事项应用中,当点击某个项目时,我需要从数组中删除该项目,为此我需要在每个onClick方法中传递项目索引或待办名称:

todos.map(el => <div key={el} onClick={this._deleteTodo.bind(this, el)}> {el} </div>)

暂时假设待办事项名称是唯一的。

根据文档所述:

这种语法的问题在于每次组件渲染时都会创建一个不同的回调函数。

问题:

如何避免在渲染方法中绑定事件或有哪些其他的替代方法?

请提供任何参考或示例,谢谢。

4个回答

38

首先:一个简单的解决方案是在map函数中创建一个组件,并将值作为props传递。当你从子组件调用该函数时,可以将该值作为props传递给传递下来的函数。

父组件

deleteTodo = (val) => {
    console.log(val)
}
todos.map(el => 
    <MyComponent val={el} onClick={this.deleteTodo}/> 

)

MyComponent

class MyComponent extends React.Component {
    deleteTodo = () => {
        this.props.onClick(this.props.val);
    }
    render() {
       return <div  onClick={this.deleteTodo}> {this.props.val} </div>
    }
}

示例代码片段

class Parent extends React.Component {
     _deleteTodo = (val) => {
        console.log(val)
    }
    render() {
        var todos = ['a', 'b', 'c'];
        return (
           <div>{todos.map(el => 
             <MyComponent key={el} val={el} onClick={this._deleteTodo}/> 
        
           )}</div>
        )
    }
    
   
}

class MyComponent extends React.Component {
        _deleteTodo = () => {
                     console.log('here');   this.props.onClick(this.props.val);
        }
        render() {
           return <div onClick={this._deleteTodo}> {this.props.val} </div>
        }
    }
    
ReactDOM.render(<Parent/>, document.getElementById('app'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="app"></div>

编辑:

第二点: 另一种方法是使用记忆化并返回一个函数

constructor() {
    super();
    this._deleteTodoListener = _.memoize(
                   this._deleteTodo, (element) => {
                        return element.hashCode();
                    }
              )
}

_deleteTodo = (element) => {
   //delete handling here
}

并像使用它一样使用它

todos.map(el => <div key={el} onClick={this._deleteTodoListener(el)}> {el} </div>)

P.S. 然而,这并不是最佳解决方案,仍然会创建多个函数,但仍比初始情况有所改善。

Third: 然而,更适当的解决方案是在最上层的 div 上添加一个 attribute 并从 event 中获取该值,如下所示:

_deleteTodo = (e) => {
     console.log(e.currentTarget.getAttribute('data-value'));

 }

 todos.map(el => <div key={el} data-value={el} onClick={this._deleteTodo}> {el} </div>)

然而,在这种情况下,属性将使用 toString 方法转换为字符串,因此对象将被转换为 [Object Object],而数组如 ["1" , "2", "3"] 则会被转换为 "1, 2, 3"


是的,我们可以做到这一点,但是我们需要创建单独的组件并建立父子关系。我认为这样做在大型应用程序中不太可扩展,因为我们通常需要在多个地方进行此类绑定。 - Mayank Shukla
2
我也曾经为此苦恼过,我的结论是,如果这种函数的重新创建会拖慢您的应用程序(我猜如果您有足够大的数据集需要重新渲染),那么您应该对这些组件采取这种方法。否则,这对性能来说并不是真正的问题,因此可以安全地忽略它。 - Kris Selbekk
是的,但这样你就可以避免你想要的东西,可扩展性在这里不应该是一个问题。 - Shubham Khatri
@kojow7 感谢您指出这个问题,这是由于从问题中复制和粘贴代码并添加自己的代码导致的一个打字错误。 - Shubham Khatri
1
@akshaykishore,在这种情况下,你可以使用第三种方法,而不是将索引传递给 onClick。 - Shubham Khatri
显示剩余3条评论

5
如何避免在渲染方法中使用此绑定方式,或者说有什么替代方法?
如果您关心重新渲染,则shouldComponentUpdate和PureComponent是您的好朋友,它们将帮助您优化渲染。
您需要从“父”组件中提取“子”组件,并始终传递相同的props并实现shouldComponentUpdate或使用PureComponent。我们想要的情况是,当我们删除一个子组件时,其他子组件不应该被重新渲染。
示例:
import React, { Component, PureComponent } from 'react';
import { render } from 'react-dom';

class Product extends PureComponent {
  render() {
    const { id, name, onDelete } = this.props;

    console.log(`<Product id=${id} /> render()`);
    return (
      <li>
        {id} - {name}
        <button onClick={() => onDelete(id)}>Delete</button>
      </li>
    );
  }
}

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      products: [
        { id: 1, name: 'Foo' },
        { id: 2, name: 'Bar' },
      ],
    };

    this.handleDelete = this.handleDelete.bind(this);
  }

  handleDelete(productId) {
    this.setState(prevState => ({
      products: prevState.products.filter(product => product.id !== productId),
    }));
  }

  render() {
    console.log(`<App /> render()`);
    return (
      <div>
        <h1>Products</h1>
        <ul>
          {
            this.state.products.map(product => (
              <Product 
                key={product.id}
                onDelete={this.handleDelete}
                {...product}
              />
            ))
          }
        </ul>
      </div>
    ); 
  }
}

render(<App />, document.getElementById('root'));

演示:https://codesandbox.io/s/99nZGlyZ

预期行为

  • <App />渲染()
  • <Product id=1...>渲染()
  • <Product id=2...>渲染()

当我们移除<Product id=2 ...时,只有<App />被重新渲染。

  • 渲染()

要在演示中看到这些消息,请打开开发者工具控制台。

相同的技术已经被用于并且描述在文章中:React is Slow, React is Fast: Optimizing React Apps in Practice by François Zaninotto.


谢谢您的建议,但我认为使用唯一键可以解决这个问题。我们想要的是当我们移除一个子元素时,其他子元素不应该重新渲染,因为我只想呈现一个带有文本的单个div。当组件很大并且我们想避免重新渲染它时,这种方法将发挥重要作用。 - Mayank Shukla
使用key属性无法解决此问题,请查看:https://codesandbox.io/s/xVZ7pL6E 即使您使用key属性,其他<Product />的render()也会被调用。演示和链接之间唯一的区别是Product extends Component而不是PureComponent - Dawid Karabin

3
这个答案https://dev59.com/BlcO5IYBdhLWcg3w7lrq#45053753已经非常详尽,但我认为与其只是重新创建小回调函数,抵御过多的重渲染会带来更大的性能提升。这通常可以通过在子组件中实现适当的shouldComponentUpdate来实现。
即使属性完全相同,以下代码仍将重新渲染子元素,除非它们在自己的shouldComponentUpdate中阻止了重新渲染(可能是从PureComponent继承而来):
handleChildClick = itemId => {}

render() {
    return this.props.array.map(itemData => <Child onClick={this.handleChildClick} data={itemData})
}

证明:https://jsfiddle.net/69z2wepo/92281/

因此,为了避免重新渲染,子组件必须实现shouldComponentUpdate。现在,唯一合理的实现是完全忽略onClick,无论它是否改变:

shouldComponentUpdate(nextProps) {
    return this.props.array !== nextProps.array;
}

在你的证明中,你调用了ReactDom.render两次。这将强制所有组件从上到下进行渲染,因此我不确定如何将其视为基于使用onClick的重新渲染的证明。实际上,你似乎在暗示避免由于事件处理程序连接而导致重新渲染的官方文档建议是错误的。 - mahonya
感谢查看我的代码!虽然我意识到我的建议实际上回答了一个不同的问题,即如何避免不必要的重新渲染而不是如何避免创建过多的函数,但是同样引用的文档在同一段落中说,过多的函数并不是什么大问题,不像不必要的重新渲染。关于我两次调用ReactDOM.render,我坚信它的行为方式相同,这里有一个类似的例子,我已经将显式重新渲染更改为由某些父状态更新引起的重新渲染:https://jsfiddle.net/7a9enxsb/1/。 - grebenyuksv

3

文档鼓励使用数据属性并从evt.target.dataset中访问它们:

_deleteTodo = (evt) => {
  const elementToDelete = evt.target.dataset.el;
  this.setState(prevState => ({
    todos: prevState.todos.filter(el => el !== elementToDelete)
  }))
}

// and from render:

todos.map(
  el => <div key={el} data-el={el} onClick={this._deleteTodo}> {el} </div>
)

此外,请注意,只有当您遇到性能问题时才有意义:

在render方法中使用箭头函数是否合适?

一般来说,是可以的,而且通常是向回调函数传递参数最简单的方法。

如果您确实遇到了性能问题,请务必进行优化!


1
由于您的回答是在2018年,现在可以使用“React Hook”进行分享。 - Isaac
你是在说 useCallback 吗? - streletss

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