事件处理

BBS 应用中的点赞功能已经涉及 React 中的事件处理,本节将详细介绍如何在 React 中处理事件。在 React 元素中绑定事件有两点需要注意:

  1. React 中,事件的命名采用驼峰命名方式,而不是 DOM 元素中的小写字母命名方式。例如,onclick 要写成 onClickonchange 要写成 onChange 等。

  2. 处理事件的响应函数要以对象的形式赋值给事件属性,而不是 DOM 中的字符串形式。例如,在 DOM 中绑定一个点击事件这样写:

<button onclick="clickButton()">
    Click
</button>

而在 React 元素中绑定一个点击事件变成这种形式:

<button onClick={clickButton} > //clickButton是一个函数
    Click
</button>

React 中的事件是合成事件,并不是原生的 DOM 事件。React 根据 W3C 规范定义了一套兼容各个浏览器的事件对象。在 DOM 事件中,可以通过处理函数返回 false 来阻止事件的默认行为,但在 React 事件中,必须显式地调用事件对象的 preventDefault 方法来阻止事件的默认行为。除了这一点外,DOM 事件和 React 事件在使用上并无差别。如果在某些场景下必须使用 DOM 提供的原生事件,可以通过 React 事件对象的 nativeEvent 属性获取。

其实,在 React 组件中处理事件最容易出错的地方是事件处理函数中 this 的指向问题,因为 ES6 class 并不会为方法自动绑定 this 到当前对象。React 事件处理函数的写法主要有三种方式,不同的写法解决 this 指向问题的方式也不同。

使用箭头函数

直接在 React 元素中采用箭头函数定义事件的处理函数,例如:

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

    render() {
        return (
          <button onClick={(event) => {console.log(this.state.number);}}>Click</button>
        );
    }
}

因为箭头函数中的 this 指向的是函数定义时的对象,所以可以保证 this 总是指向当前组件的实例对象。当事件处理逻辑比较复杂时,如果把所有的逻辑直接写在 onClick 的大括号内,就会导致 render 函数变得臃肿,不容易直观地看出组件的 UI 结构,代码可读性也不好。这时,可以把逻辑封装成组件的一个方法,然后在箭头函数中调用这个方法。代码如下:

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

    // 每点击一次 Button.state 中的 number 增加 1
    handleClick(event) {
        const number = ++this.state.number;
        this.setState({
            number: number
        });
    }

    render() {
        return (
            <div>
                <div>{this.state.number}</div>
                <button onClick={ (event) => {this.handleClick(event);}}>
                    Click
                </button>
            </div>
        );
    }
}

直接在 render 方法中为元素事件定义事件处理函数,最大的问题是,每次 render 调用时,都会重新创建一个新的事件处理函数,带来额外的性能开销,组件所处层级越低,这种开销就越大,因为任何一个上层组件的变化都可能会触发这个组件的 render 方法。当然,在大多数情况下,这点性能损失是可以不必在意的。

使用组件方法

直接将组件的方法赋值给元素的事件属性,同时在类的构造函数中,将这个方法的 this 绑定到当前对象。例如:

class MyComponent extends React.Component {
    constructor(props) {
        super(props);
        this.state = {number: 0};
        this.handleClick = this.handleClick.bind(this);
    }

    // 每点击一次 Button.state 中的 number 增加 1
    handleClick(event) {
        const number = ++this.state.number;
        this.setState({
            number: number
        });
    }

    render() {
        return (
            <div>
                <div>{this.state.number}</div>
                <button onClick={ this.handleClick }>
                    Click
                </button>
            </div>
        );
    }
}

这种方式的好处是每次 render 不会重新创建一个回调函数,没有额外的性能损失。但在构造函数中,为事件处理函数绑定 this,尤其是存在多个事件处理函数需要绑定时,这种模板式的代码还是会显得烦琐。

有些开发者还习惯在为元素的事件属性赋值时,同时为事件处理函数绑定 this,例如:

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

    // 每点击一次 Button.state 中的 number 增加 1
    handleClick(event) {
        const number = ++this.state.number;
        this.setState({
            number: number
        });
    }

    render() {
        return (
            <div>
                <div>{this.state.number}</div>
                { /* 事件属性赋值和this绑定同时 */ }
                <button onClick={ this.handleClick.bind(this) }>
                    Click
                </button>
            </div>
        );
    }
}

使用 bind 会创建一个新的函数,因此这种写法依然存在每次 render 都会创建一个新函数的问题。但在需要为处理函数传递额外参数时,这种写法就有了用武之地。例如,下面的例子需要为 handleClick 传入参数 item

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={this.handleClick.bind(this, item)}>
                                {item}
                            </li>
                        )
                    )
                }
            </ul>
        );
    }
}

属性初始化语法(property initializer syntax)

使用 ES7 的 property initializers 会自动为 class 中定义的方法绑定 this。例如:

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

    // ES7 的属性初始化语法,实际上也是使用了箭头函数
    handleClick = (event) => {
        const number = ++this.state.number;
        this.setState({
            number: number
        });
    }

    render() {
        return (
            <div>
                <div>{this.state.number}</div>
                <button onClick={ this.handleClick }>
                    Click
                </button>
            </div>
        );
    }
}

这种方式既不需要在构造函数中手动绑定 this,也不需要担心组件重复渲染导致的函数重复创建问题。但是,property initializers 这个特性还处于试验阶段,默认是不支持的。不过,使用官方脚手架 Create React App 创建的项目默认是支持这个特性的。你也可以自行在项目中引入 babeltransform-class-properties 插件获取这个特性支持。