【问题标题】:How to avoid bind or inline arrow functions inside render method如何避免在渲染方法中绑定或内联箭头函数
【发布时间】:2018-08-05 20:41:23
【问题描述】:

我们应该避免在 render 中绑定方法,因为在重新渲染期间它会创建新方法而不是使用旧方法,这会影响性能。

所以对于这样的场景:

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

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

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

或者我们可以使用property initializer syntax:

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

现在让我们考虑我们想要传递一些额外参数的情况,假设在一个简单的 todo 应用程序中,单击项目我需要从数组中删除项目,为此我需要传递项目索引或待办事项每个 onClick 方法中的名称:

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

现在只假设待办事项名称是唯一的。

根据DOC

这种语法的问题是创建了不同的回调 每次组件渲染时。

问题:

如何避免这种在 render 方法中的绑定方式或者有什么替代方法?

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

【问题讨论】:

    标签: javascript reactjs arrow-functions function-binding class-fields


    【解决方案1】:

    首先:一个简单的解决方案是为地图函数中的内容创建一个组件,并将值作为道具传递,当您从子组件调用该函数时,您可以将值传递给作为道具传递的功能。

    家长

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

    我的组件

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

    示例 sn-p

    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>

    编辑:

    第二种:另一种方法是使用 memoize 并返回一个函数

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

    附:然而,这不是一个最好的解决方案,仍然会导致 正在创建多个功能,但仍然是对 初始案例。

    第三:然而,更合适的解决方案是在最上面的 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"

    【讨论】:

    • 是的,我们可以这样做,但是我们需要创建单独的组件并放置父子关系,我认为这不会那么可扩展,因为在大型应用程序中我们曾经进行这种绑定在多个地方。
    • 我也一直在努力解决这个问题,我的结论是,如果这种功能的重新创建会减慢您的应用程序速度(这......我猜......如果你有足够大的数据集重新渲染很多),您应该对这些组件遵循这种方法。否则,这对于性能来说并不是真正的问题,因此可以安全地忽略。
    • 是的,但是如何避免你想要的和可扩展性在这里应该不是问题
    • 在创建类属性时的前两个代码块中,使用deleteTodo,但在引用它时使用_deleteTodo。这是一个错误还是下划线在这里有什么特殊用途?
    • @akshaykishore,在这种情况下,您可以使用第三种方法,而不是将索引传递给 onClick
    【解决方案2】:

    如何避免这种在 render 方法中的绑定方式或者什么是 这个的替代品?

    如果您关心重新渲染,那么 shouldComponentUpdatePureComponent 是您的朋友,他们会帮助您优化渲染。

    您必须从“父”中提取“子”组件并始终传递相同的道具并实现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

    预期行为

    • &lt;App /&gt; render()
    • &lt;Product id=1... render()
    • &lt;Product id=2... render()

    当我们删除 &lt;Product id=2 ... 时,只有 &lt;App /&gt; 会重新渲染。

    • 渲染()

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

    François Zaninotto 的文章:React is Slow, React is Fast: Optimizing React Apps in Practice 使用并描述了相同的技术。

    【讨论】:

    • 感谢您的建议,但我认为使用唯一键可以解决这个问题我们想要的是当我们删除一个孩子时,其他孩子不应该重新渲染 因为我只想用文本呈现单个 div。当组件很大并且我们希望避免重新渲染它时,这种方法将发挥重要作用。
    • 使用key属性并不能解决这个问题,看:codesandbox.io/s/xVZ7pL6E即使你使用key属性,其他&lt;Product /&gt;的render()也会被调用。 demo和那个链接之间的唯一变化是Product extends Component而不是PureComponent
    【解决方案3】:

    Documentation 鼓励使用 data-attributes 并从 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>
    )
    

    还有note,这仅在您遇到性能问题时才有意义:

    可以在渲染方法中使用箭头函数吗?

    一般来说,是的,没关系,而且这通常是最简单的方法 将参数传递给回调函数。

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

    【讨论】:

    • 由于你的答案现在是 2018 年,只是为了分享现在你可以使用“React Hook”
    • 你说的是useCallback吗?
    【解决方案4】:

    这个答案https://stackoverflow.com/a/45053753/2808062 绝对是详尽无遗的,但我会说与过度的重新渲染作斗争而不是仅仅重新创建微小的回调会给您带来更多的性能改进。这通常通过在子组件中实现正确的shouldComponentUpdate 来实现。

    即使 props 完全相同,以下代码仍会重新渲染孩子,除非他们在自己的 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 的方法重新渲染的证据。实际上,您似乎建议官方文档建议避免由于事件处理程序接线而重新渲染是错误的。
    • 感谢您查看我的代码!好吧,虽然我意识到我的建议实际上回答了一个不同的问题,即如何避免不必要的重新渲染而不是如何避免创建多余的功能,但同一个引用的文档在同一段中说多余的功能没什么大不了的,不像不必要的重新渲染。关于我两次调用 ReactDOM.render,我坚信它的行为方式相同,这是一个类似的示例,我已将显式重新渲染更改为由某些父状态更新引起的重新渲染:jsfiddle.net/7a9enxsb/1
    猜你喜欢
    • 2022-01-25
    • 1970-01-01
    • 1970-01-01
    • 2019-08-14
    相关资源
    最近更新 更多