React 为循环创建的元素绑定事件处理并传参

3,418 阅读4分钟

场景

设想我们现在有一个很长很长的数组,要根据这个数组来循环创建一组元素,每个元素上需要绑定点击事件处理程序;我们需要在事件处理函数中拿到当前被点击元素对应的数组项信息。

官方操作

React官方文档对于这种场景给出了常用处理方案

在循环中,通常我们会为事件处理函数传递额外的参数。例如,若 id 是你要删除那一行的 ID,以下两种方式都可以向事件处理函数传递参数:

<button onClick={(e) => this.deleteRow(id, e)}>Delete Row</button>
<button onClick={this.deleteRow.bind(this, id)}>Delete Row</button>

上述两种方式是等价的,分别通过箭头函数和 Function.prototype.bind 来实现。

在这两种情况下,React 的事件对象 e 会被作为第二个参数传递。如果通过箭头函数的方式,事件对象必须显式的进行传递,而通过 bind 的方式,事件对象以及更多的参数将会被隐式的进行传递。

官方操作的问题

上述方法存在的最明显的问题在于,无论是使用箭头函数,还是使用.bind(),每次render的时候都会重新创建事件处理函数。

箭头函数自不必说。而在render中使用.bind(),其实等同于每次render的时候,React都要对数组每一项先执行一下这个.bind(),拿它返回的结果当做此元素绑定的事件处理方法。显然,每一次执行.bind()返回的都是一个新的值(这和在constructor中先bind一次,然后render中直接使用bind的结果原理不同)。

如果我们的数组有100多项,那每一次render的时候就要重新创建100多个事件处理函数。更糟糕的是,如果我们是将事件处理函数作为 prop 传入循环创建的子组件,可能会导致这些子组件都进行额外的重新渲染。

优化思路

我们希望循环创建的所有元素,其绑定的事件处理函数都指向同一个函数对象,并且不会在每次render 的时候发生变化。这就要求我们不能把循环项中的信息作为入参直接传入事件处理函数

当事件处理函数执行时,我们一定能获取到的信息是当前触发事件的event对象。所以我们可以考虑从event对象中获取我们需要的信息,而非显式地传入参数。

如何在event对象中拿到与当前数组项对应的信息呢?

dataset

HTMLElement.dataset属性允许无论是在读取模式和写入模式下访问在 HTML或 DOM中的元素上设置的所有自定义数据属性(data-*)集。

看一下MDN上的示例就明白了:

<div id="user" data-id="1234567890" data-user="johndoe" data-date-of-birth>John Doe
</div>

var el = document.querySelector('#user');

// el.id == 'user'
// el.dataset.id === '1234567890'
// el.dataset.user === 'johndoe'
// el.dataset.dateOfBirth === ''

因此,我们可以进行这样的操作:在render中,为每一项循环创建的元素添加dataset属性,将当前数组项的信息绑定上去。然后在事件处理函数中通过event.target.dataset取到绑定的信息

如果数组对象数据较为复杂,直接绑定在dataset上会导致对应的html标签挂上一长串信息,而且都是string格式,拿到之后还要再费力解析。所以我们也可以只绑定数组项对应的索引,然后在事件处理函数中根据索引去取到对应的数组对象。

实践示例

import React, { Component } from "react";
class ClassComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      curText: "",
      toClickList: [
        {
          id: 1,
          text: "我是1",
        },
        {
          id: 2,
          text: "我是2",
        },
        {
          id: 3,
          text: "我是3",
        },
      ],
    };
    this.setText = this.setText.bind(this);
  }

  // 事件处理函数
  setText(event) {
    const index = event.target.dataset.index
    const item = this.state.toClickList[index];

    this.setState({
      curText: item.text
    })
  }

  render() {
    const { toClickList } = this.state;
    return (
      <div>
        {toClickList.map((item, index) => {
          return (
            {/* 在循环中为每一项元素绑定dataset属性。这里我们把index绑上去 */}
            <button onClick={this.setText} key={item.id} data-index={index}>
              {item.text}
            </button>
          );
        })}
        <br/>

        当前点击的text:{this.state.curText}
      </div>
    );
  }
}

export default ClassComponent;

总结

我们通过在循环中为每一项元素绑定dataset属性,把数组对应项的索引index绑定在DOM上,再在事件处理函数中通过event.target.dataset.index取到当前事件对应数组元素的索引,进而取到原数组下的对象信息。

其实不只是在React的class组件中,在其他任何需要给循环创建的元素绑定事件监听的场景中,都可以通过这种思路来避免重复创建事件处理函数。

参考目录

React 事件处理

React Dynamic Events