w3ctech

React 初学者教程 10:React 中的事件

概述:通过学习如何处理事件,学习如何将无趣的 React 应用变为有趣而具互动性的应用。

迄今为止,我们所有的示例都是只在页面加载时执行。你可能会猜到,这是不正常的。在很多应用中,特别是重 UI 类型的应用中,应用程序要做的很多事情只是对某种事情的响应。这里,某种事情可能是被鼠标点击、按键、窗口缩放、或者其它手势操作以及交互。而让所有这一切变得可能的粘合剂是事件

现在,你可能从 DOM世界中使用事件的体验中已经熟悉了事件。(如果没有的话,那么我推荐你先快速复习一下)。React 处理事件的方式有点不同,如果没有密切关注的话,这些不同之处会以不同的方式让你吃惊。不要担心。这就是为什么要有本教程的原因。我们先从几个简单的示例开始,然后逐步看看更多古怪的、复杂的、烦人的事情。

监听和响应事件

学习 React 事件最简单的方式就是用它。这里我们先做一个简单的示例,每次点击按钮后,计数器增一:

每次点击 + 按钮,计数器就增加 1。点击几次按钮后,就是这样子的:

在幕后,本例的工作方式很简单。每次点击按钮,就触发一个事件。我们监听该事件,当事件被监听到时,做各种 React 干的事情,让计数器更新。

起点

为节省点时间,我们不打算从头开始创建所有东西。现在你可能已经知道如何用 组件样式状态等等。但是,我们打算从一个部分实现的示例开始,该示例包含了除了我们这里将要学习的事件相关的功能之外的所有东西。

首先创建一个新 HTML 文档:

<!DOCTYPE html>
<html>

<head>
  <title>React! React! React!</title>
  <script src="https://fb.me/react-15.0.0-rc.2.js"></script>
  <script src="https://fb.me/react-dom-15.0.0-rc.2.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>

  <style>
    #container {
      padding: 50px;
      background-color: #FFF;
    }
  </style>
</head>

<body>
  <div id="container"></div>
  <script type="text/babel">

  </script>
</body>
</html>

然后,在 script 标记中添加如下代码:

var destination = document.querySelector("#container");

var Counter = React.createClass({
  render: function() {
      var textStyle = {
        fontSize: 72,
        fontFamily: "sans-serif",
        color: "#333",
        fontWeight: "bold"
      };

      return (
        <div style={textStyle}>
          {this.props.display}
        </div>
      );
    }
});

var CounterParent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  render: function() {
      var backgroundStyle = {
        padding: 50,
        backgroundColor: "#FFC53A",
        width: 250,
        height: 100,
        borderRadius: 10,
        textAlign: "center"
      };

      var buttonStyle = {
        fontSize: "1em",
        width: 30,
        height: 30,
        fontFamily: "sans-serif",
        color: "#333",
        fontWeight: "bold",
        lineHeight: "3px"
      };

      return (
        <div style={backgroundStyle}>
          <Counter display={this.state.count}/>
          <button style={buttonStyle}>+</button>
        </div>
      );
    }
});

ReactDOM.render(
  <div>
    <CounterParent/>
  </div>,
  destination
);

完成这段代码后,在浏览器中预览一下,看看代码是否正确。然后,花几分钟看看这些代码是干嘛的。这些代码都是我们前面学习过的,所有代码看起来都不应该是陌生的。当然,唯一古怪的事情是点击按钮什么都不会发生,下面我们马上来解决这个问题。

让按钮点击做点事情

每次点击加号按钮,我们想让计数器的值增一。我们要做的事情大致是这样的:

  1. 监听按钮上的点击事件。
  2. 当点击事件被监听到时,指定处理该事件的事件处理器。
  3. 实现事件处理器,增加 this.state.count 属性的值。

我们将直接沿着列表做下去...先从监听 click 事件开始。在 React 中,我们是通过在 JSX 中以内联(inline)的方式指定一切来监听一个事件。更特殊地,是在标记内同时指定要监听的事件以及要调用的事件处理器。为此,我们找到 CounterParent 组件内的 return 函数,作出如下修改:

  .
  .
  .
return (
  <div style={backgroundStyle}>
    <Counter display={this.state.count}/>
    <button onClick={this.increase} style={buttonStyle}>+</button>
  </div>
);

这里我们所做的是,告诉 React 在监听到 onClick 事件后,调用 increase 函数。

下一步,我们来实现 increase 函数,即事件处理器。在 CounterParent 组件内,添加如下高亮度代码:

var CounterParent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  increase: function(e) {
    this.setState({
      count: this.state.count + 1
    });
  },
  render: function() {
      var backgroundStyle = {
        padding: 50,
        backgroundColor: "#FFC53A",
        width: 250,
        height: 100,
        borderRadius: 10,
        textAlign: "center"
      };

      var buttonStyle = {
        fontSize: "1em",
        width: 30,
        height: 30,
        fontFamily: "sans-serif",
        color: "#333",
        fontWeight: "bold",
        lineHeight: "3px"
      };

      return (
        <div style={backgroundStyle}>
          <Counter display={this.state.count}/>
          <button onClick={this.increase} style={buttonStyle}>+</button>
        </div>
      );
    }
});

这几行代码的作用是确保每次对 increase 函数的调用都会给 this.state.count 属性的值加一。因为我们是在处理事件,所以 increase 函数(作为签名的事件处理器)可以访问任何事件参数。

我们已经设置这些参数可以通过 e 来访问,并且通过查看 increase 函数的签名(即该函数的声明)看到。我们很快将学习不同的事件及其属性。

现在继续,在浏览器中预览一下应用。当应用被加载,点击加号按钮,看看新添加的代码起作用。我们计数器值会随着每次点击增加!这难道不是很棒的吗?

Event 属性

我们知道,事件传递事件参数给事件处理器。这些事件参数包含了大量我们要处理的针对特定事件类型的属性。在常规 DOM 中,每个事件都有自己的类型。例如,如果处理鼠标事件,那么事件和事件参数对象将是 MouseEvent 类型的。这个 MouseEvent 对象让我们访问鼠标相关的信息,比如哪个按钮被按下,或者鼠标点击的屏幕位置。与键盘相关的事件参数是 KeyboardEvent 类型的。KeyboardEvent 对象包含一些属性,这些属性让我们可以判断哪个键被按下。还有很多事件类型,我可以继续列举下去。但是你应该已经明白了。每个事件类型都包含了一套它自己的属性,我们可以通过事件处理器访问这些属性。

那么,为什么我要用你已经知道的事情烦你呢?

合成事件

在 React 中,如果像在 onClick 中所做的那样在 JSX 中指定事件时,是不能直接处理常规的 DOM 事件的,而是处理 React 特定的事件类型 SyntheticEvent。你的事件处理器不能得到原生的事件参数类型 MouseEvent、KeyboardEvent 等等,而是总是得到封装了浏览器的原生事件的事件参数类型 SyntheticEvent。那么这对我们代码有什么影响呢?令人吃惊的是完全没有影响。

每个 SyntheticEvent 包含如下属性:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
DOMEventTarget target
number timeStamp
string type

这些属性看起来很简单,很通用。而不通用的部分取决于 SyntheticEvent 包含了什么类型的本地事件。这意味着一个包含了 MouseEvent 的 SyntheticEvent 可以访问像如下的这些鼠标相关的属性:

boolean altKey
number button
number buttons
number clientX
number clientY
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
number pageX
number pageY
DOMEventTarget relatedTarget
number screenX
number screenY
boolean shiftKey

同样,包含了 KeyboardEvent 的 SyntheticEvent 就能访问与键盘相关的特殊属性:

boolean altKey
number charCode
boolean ctrlKey
boolean getModifierState(key)
string key
number keyCode
string locale
number location
boolean metaKey
boolean repeat
boolean shiftKey
number which

最后,这一切意味着我们依然可以在 SyntheticEvent 中用在原生 DOM 世界具备的相同的功能。

现在,这里有一些我费了一番苦功才学到的东西。当使用合成事件及其属性时,不要参考传统 DOM 事件文档。因为 SyntheticEvent 包含了原生 DOM 事件,事件及其属性并不一定是一对一映射的。有些 DOM 事件在 React 甚至是不存在的。要避免遇到任何问题,如果你想知道一个合成事件或者任何其属性,应该参考 React 事件系统文档

用事件属性干活

现在,你可能已经看到比你可能喜欢的更多的有关 DOM 和合成事件。我们开始写一些代码,将新发现的知识放在一起很好的使用。现在,我们的计数器通过每次点击加号按钮加一。我们想做的是当用鼠标点击加号按钮同时按着 Shift 键,让计数器加十。

我们打算在用鼠标时,用 SyntheticEvent 的 shiftKey 属性完成这件事情:

boolean altKey
number button
number buttons
number clientX
number clientY
boolean ctrlKey
boolean getModifierState(key)
boolean metaKey
number pageX
number pageY
DOMEventTarget relatedTarget
number screenX
number screenY
boolean shiftKey

这个属性的工作方式很简单。当鼠标事件触发时,如果 Shift 键被按下,那么 shiftKey 属性的值为 true,否则为 false。在 Shift 键被按下时,要让计数器加十,请回到 increase 函数,作出如下高亮度行的修改:

increase: function(e) {
    var currentCount = this.state.count;
    if (e.shiftKey) {
        currentCount += 10;
    } else {
        currentCount += 1;
    }
    this.setState({
        count: currentCount
    });
},

之后,在浏览器中预览我们的示例。每次点击加号按钮,计数器就会加一。如果点击加号按钮,同时按住 Shift 键,计数器就会加十。

这一切能够运行的原因是我们根据 Shift 键是否被按下,修改了增加的行为。这主要是通过如下行来处理的:

if (e.shiftKey) {
  currentCount += 10;
} else {
  currentCount += 1;
}

如果 SyntheticEvent 事件参数上的 shiftKey 属性为 true,那么计数器就增加10.如果 shiftKey 的值为 false,那么就只加一。

更多事件技巧

还没完!迄今为止,我们是以一种过份简单的方式来观察在 React 中如何处理事件。在真实世界中,几乎很少会有事情像我们看到的那样直接。真实的应用将是更复杂,并且因为 React 坚持独立特行,所以我们要学习(或者重新学习)一些与事件有关的新技巧和技术,来让应用运行。这就是本节的由来。我们将看看一些你会遇到的常见的情况,以及如何处理它们。

不能直接在组件上监听事件

假设组件只是一个按钮或者其它用户要交互的 UI 元素类型,那么你就逃不了要做一些像如下高亮度行中看到的一样的事情:

var CounterParent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  increase: function() {
    this.setState({
      count: this.state.count + 1
    });
  },
  render: function() {
    return (
      <div>
        <Counter display={this.state.count}/>
        <PlusButton onClick={this.increase}/>
      </div>
    );
  }
});

表面上看,这行 JSX 是完全有效的。当有人点击 PlusButton 组件时,increase 函数会被调用。如果你好奇的话,那么如下是 PlusButton 组件看起来的样子:

var PlusButton = React.createClass({
  render: function() {
      return (
        <button>
          +
        </button>
      );
    }
});

PlusButton 并没有做啥特殊的事情,它只是一个简单的 HTML 元素!

无论你如何折腾,这一切都不重要。通过组件返回的 HTML 看起来如何简单或者明显这无关紧要。你只是不能直接在组件上监听事件。原因是,组件是 DOM 元素的包装器。要监听一个组件上的事件到底是什么意思?一旦组件被展开到 DOM 元素中,你要监听的事件是放到外层 HTML 元素上吗?它是一些其它元素吗?在监听一个事件与声明你要监听的一个 prop 之间你如何区分?

这些问题都没有清晰的答案。过份点说,解决方案是要么就不监听组件上的事件。幸运的是,有一个变通方案,就是将事件处理器当作是一个 prop,并将它传递给组件。然后在组件内,我们可以把事件赋值给一个 DOM 元素,并将事件处理器设置为我们刚传进来的 prop 的值。我们来看一个示例。

var CounterParent = React.createClass({
    .
    .
    .
  render: function() {
    return (
      <div>
        <Counter display={this.state.count}/>
        <PlusButton clickHandler={this.increase}/>
      </div>
    );
  }
});

在本例中,我们创建一个属性 clickHandler,该属性的值是 increase 事件处理器。然后,在 PlusButton 组件内,我们可以像这样做:

var PlusButton = React.createClass({
  render: function() {
      return (
        <button onClick={this.props.clickHandler}>
          +
        </button>
      );
    }
});

在 button 元素上,我们指定了 onClick 事件,并将它的值设置为 clickHandler prop。在运行时,这个 prop 被求值为 increase 函数,并且点击加号按钮确保 increase 函数被调用。这就解决了我们的问题,同时还让组件全程参与了。

监听常规 DOM 事件

如果你认为前面的小节是很好的东西,那么等着看看我们这里有什么。并非所有的 DOM 事件都有 SyntheticEvent 的等价物。看起来当在 JSX 中在行内指定事件时,好像你只要添加一个 on 前缀,并且把要监听的事件首字母大写就可以了:

var Something = React.createClass({
  handleMyEvent: function(e) {
    // do something
  },
  render: function() {
      return (
        <div myWeirdEvent={this.handleMyEvent}>Hello!</div>
      );
    }
});

但是这样行不通!对于那些 React 官方不能识别的事件,你必须用传统的 addEventListener, 加上一些额外的手段。

看看如下的代码:

var Something = React.createClass({
  handleMyEvent: function(e) {
    // do something
  },
  componentDidMount: function() {
    window.addEventListener("someEvent", this.handleMyEvent);
  },
  componentWillUnmount: function() {
    window.removeEventListener("someEvent", this.handleMyEvent);
  },
  render: function() {
      return (
        <div>Hello!</div>
      );
    }
});

这里我们的 Something 组件要监听一个 someEvent 事件。我们在 componentDidMount 方法下开始监听该事件,我们知道该方法是在组件被渲染时自动调用的。监听该事件的方式是,用 addEventListener,并同时指定事件和要调用的事件处理器:

var Something = React.createClass({
  handleMyEvent: function(e) {
    // do something
  },
  componentDidMount: function() {
    window.addEventListener("someEvent", this.handleMyEvent);
  },
  componentWillUnmount: function() {
    window.removeEventListener("someEvent", this.handleMyEvent);
  },
  render: function() {
      return (
        <div>Hello!</div>
      );
    }
});

这应该是很简单。唯一要记住的事情是,当组件要被销毁时,要移除事件监听器。为此,你可以用 componentDidMount 相反的方法 componentWillUnmount 方法。在该方法内,放上 removeEventListener 调用,以确保在组件消失后该事件监听的痕迹也消失。

事件处理器中 this 的含义

当在 React 中处理事件时,事件处理器中 this 的值,与非 React 的 DOM 世界中常看到的,是不同的。在非 React 的世界中,事件处理器中 this 的值引用的是触发该事件的元素:

function doSomething(e) {
  console.log(this); //button element
}

var foo = document.querySelector("button");
foo.addEventListener("click", doSomething, false);

在 React 世界中(即组件是用 React.createClass 创建的),事件处理器中的 this 引用的总是事件处理器所处的组件:

var CounterParent = React.createClass({
  getInitialState: function() {
    return {
      count: 0
    };
  },
  increase: function(e) {
    console.log(this); // CounterParent component

    this.setState({
      count: this.state.count + 1
    });
  },
  render: function() {
      return (
        <div>
          <Counter display={this.state.count}/>
          <button onClick={this.increase}>+</button>
        </div>
      );
    }
});

在本例中,increase 事件处理器中 this 的值为 CounterParent 组件,而不是触发该事件的元素。这是因为 React 自动绑定一个组件中所有的方法给 this。这种自动绑定的行为只适用于当组件是用 React.createClass 创建时。如果用 ES6 的类来定义组件,那么事件处理器中 this 的值就是 undefined,除非你自己显式绑定它:

<button onClick={this.increase.bind(this)}>+</button>

ES6 中自动绑定是不会发生的,所以如果你不用 React.createClass 创建组件,必须记住这点。

React...为什么?为什么?!

就今天就到这儿之前,我们来谈谈为什么 React 决定背离我们过去处理事件的方式。有两个原因:

  1. 浏览器兼容性
  2. 性能提升

我们稍微详细阐述一下这两个原因。

1. 浏览器兼容性

事件处理在现代浏览器中是可以一致工作的事情之一,但是如果回到旧的浏览器版本,事情就会马上变得很糟糕。通过将所有本地事件封装为一个 SyntheticEvent 类型的对象,React 让我们从处理最终不得不处理的事件处理怪癖中解脱出来。

2. 提升性能

在复杂的 UI 中,事件处理器越多,应用程序占用的内存就越多。虽然手动处理这并不难,但是这有点枯燥,因为你得设法将事件组织到一个共同的父之下。有时,这是不可能的。有时,麻烦超过了好处。而 React 就做的很聪明。

React 从不会将事件处理器直接绑定到 DOM 元素。它在文档的根部使用一个事件处理器,来负责监听所有事件,并按需调用合适的事件处理器:

这就将你从不得不自己处理优化事件处理器相关代码中解脱出来。如果过去你不得不手动干这事,那么你可以宽心地知道 React 为你搞定这些枯燥的任务。如果你还从没有自己优化过事件处理相关的代码,那么你就幸福了 :P

总结

你将花费很多事件处理事件,本教程已经给了你很多东西。我们以学习如何监听事件以及指定事件处理器的基础知识开始。快结束时,我们都在深水区,看到事件的箱子角,如果不足够的仔细的话,就会撞到它。我不会像撞到角的。这不好玩。

w3ctech微信

扫码关注w3ctech微信公众号

共收到0条回复