React 深入系列 5:事件处理

2021/5/14 React

原文:React 深入系列5:事件处理 (opens new window)

React 深入系列,深入讲解了 React 中的重点概念、特性和模式等,旨在帮助大家加深对 React 的理解,以及在项目中更加灵活地使用 React。

Web 应用中,事件处理是重要的一环,事件处理将用户的操作行为转换为相应的逻辑执行或界面更新。在 React 中,处理事件响应的方式有多种,本文将详细介绍每一种处理方式的用法、使用场景和优缺点。

# 使用匿名函数

先上代码:

//代码 1
class MyComponent extends React.Component {
  render() {
    return (
      <button onClick={()=>{console.log('button clicked');}}>
        Click
      </button>
    );
  }
}
1
2
3
4
5
6
7
8
9
10

点击 Button 的事件响应函数是一个匿名函数,这应该是最常见的处理事件响应的方式了。这种方式的好处是,简单直接。哪里需要处理事件响应,就在哪里定义一个匿名函数处理。代码 1 中的匿名函数使用的是箭头函数,我们也可以不使用箭头函数:

//代码 2
class MyComponent extends React.Component {
  render() {
    return (
      <button onClick={function(){console.log('button clicked');}}>
        Click
      </button>
    );
  }
}
1
2
3
4
5
6
7
8
9
10

虽然代码 2 的运行效果和代码 1 相同,但实际项目中很少见到代码 2 的这种写法。这是因为箭头函数解决了 this 绑定的问题,可以将函数体内的 this 绑定到当前对象,而不是运行时调用函数的对象。如果响应函数中需要使用 this.state,那么代码 2 就无法正常运行了。所以项目中一般直接使用箭头函数定义的匿名函数作为事件响应。

使用匿名函数的缺点是:当事件响应逻辑比较复杂时,匿名函数的代码量会很大,会导致 render 函数变得臃肿,不容易直观地看出组件最终渲染出的元素结构。另外,每次 render 方法调用时,都会重新创建一个匿名函数对象,带来额外的性能开销,当组件的层级越低时,这种开销就越大,因为任何一个上层组件的变化都可能会触发这个组件的 render 方法。当然,在大多数情况下,这点性能损失是可以不必在意的。

# 使用组件方法

代码如下:

//代码 3
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {number: 0};
    this.handleClick = this.handleClick.bind(this); // 手动绑定 this
  }

  handleClick() {
    this.setState({
      number: ++this.state.number
    });
  }
  
  render() {
    return (
      <div>
        <div>{this.state.number}</div>
        <button onClick={this.handleClick}>
          Click
        </button>
      </div>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

点击 Button 的事件响应函数是组件的方法:handleClick。这种方式的好处是:每次 render 方法的调用,不会重新创建一个新的事件响应函数,没有额外的性能损失。但是,使用这种方式要在构造函数中为作为事件响应的方法 (handleClick),手动绑定 this: this.handleClick = this.handleClick.bind(this),这是因为 ES6 语法的缘故,ES6 Class 的方法默认不会把 this 绑定到当前的实例对象上,需要我们手动绑定。每次都手动绑定 this 是不是有点繁琐?好吧,让我们来看下一种方式。

# 使用属性初始化语法

使用 ES7 的属性初始化语法( property initializers ),代码可以这样写:

//代码 4
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {number: 0};
  }

  handleClick = () => {
    this.setState({
      number: ++this.state.number
    });
  }
  
  render() {
    return (
      <div>
        <div>{this.state.number}</div>
        <button onClick={this.handleClick}>
          Click
        </button>
      </div>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

这样一来,再也不用手动绑定 this 了。但是你需要知道,这个特性还处于试验阶段,默认是不支持的。如果你是使用官方脚手架 Create React App (opens new window) 创建的应用,那么这个特性是默认支持的。你也可以自行在项目中引入 babel 的 transform-class-properties (opens new window) 插件获取这个特性支持。

# 事件响应函数的传参问题

事件响应函数默认是会被传入一个事件对象 Event 作为参数的。如果想传入其他参数给响应函数应该怎么办呢?

使用第一种方式的话很简单,直接使用新参数:

//代码 5
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [1,2,3,4],
      current: 1
    };
  }
  
  handleClick(item,event) {
    this.setState({
      current: item
    });
  }

  render() {
    return (
      <ul>
        {this.state.list.map(
          (item)=>(
            <li className={this.state.current === item ? 'current':''} 
            onClick={(event) => this.handleClick(item, event)}>{item}
            </li>
          )
        )}
      </ul>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

onClick 的响应函数中,方法体内可以直接使用新的参数 item。

使用第二种方式的话,可以把绑定 this 的操作延迟到 render 中,在绑定 this 的同时,绑定额外的参数:

//代码 6
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [1,2,3,4],
      current: 1
    };
  }
  
  handleClick(item) {
    this.setState({
      current: item
    });
  }

  render() {
    return (
      <ul>
        {this.state.list.map(
          (item)=>(
            <li className={this.state.current === item ? 'current':''} 
            onClick={this.handleClick.bind(this, item)}>{item}
            </li>
          )
        )}
      </ul>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

使用第三种方式,解决方案和第二种基本一致:

//代码 7
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      list: [1,2,3,4],
      current: 1
    };
  }
  
  handleClick = (item) =>  {
    this.setState({
      current: item
    });
  }

  render() {
    return (
      <ul>
        {this.state.list.map(
          (item)=>(
            <li className={this.state.current === item ? 'current':''} 
            onClick={this.handleClick.bind(undefined, item)}>{item}
            </li>
          )
        )}
      </ul>
    );
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

不过这种方式就有点鸡肋了,因为虽然你不需要通过 bind 函数绑定 this,但仍然要使用 bind 函数来绑定其他参数。

关于事件响应函数,还有一个地方需要注意。不管你在响应函数中有没有显式的声明事件参数 Event,React 都会把事件 Event 作为参数传递给响应函数,且参数 Event 的位置总是在其他自定义参数的后面。例如,在代码 6 和代码 7 中,handleClick的参数中虽然没有声明 Event 参数,但你依然可以通过arguments[1]获取到事件 Event 对象。

代码 7 定义的事件处理器用的是「属性初始化语法(不是原型上的方法,自有属性上的方法)」,这样的方法是无法使用 arguments 的,arguments只在函数和 class方法里边起作用!

总结一下,三种事件处理的方式,第一种有额外的性能损失;第二种需要手动绑定 this,代码量增多;第三种用到了 ES7 的特性,目前并非默认支持,需要 Babel 插件的支持,但是写法最为简洁,也不需要手动绑定 this。一般推荐使用第二种和第三种方式。


# 总结

image-20210311234441161

组件方法:

  • (e) => this.xxx(6,e)
  • 确定e的位置
    • this.xxx.bind(this,6) -> event:arguments[1]
    • this.xxx.bind(this,6,7) -> event:arguments[2]

属性初始化语法(这种不好使,在参数问题上反直觉):

  • this.x.bind(undefined,6)(你指定thisundefined,但属性初始化语法里边的this依旧是组件实例) -> 无法拿到 arguments,所以无法拿到 e,除非你包一层函数上去,然后传参,像上边那样……
    • 如:(e) => this.x(6,e)
上次更新: 2022/3/30 17:08:51