事件处理与合成事件(react)

3,344 阅读8分钟

js

React 元素的事件处理和 DOM 元素的很相似,但是有一点语法上的不同:

  • React 事件的命名采用小驼峰式(camelCase),而不是纯小写。

  • 使用 JSX 语法时你需要传入一个函数作为事件处理函数,而不是一个字符串。

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

  • 匿名函数
// 代码1
class MyComponent extends React.Component {
  render() {
    return <button onClick={()=> console.log('button')}>Click</button>;
  }
}

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

//代码2
class MyComponent extends React.Component {
  render() {
    return <button onClick={function(){console.log('button');}}>Click</button>;
  }
}

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

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

  • 组件方法
//代码3
class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this); // 手动绑定this
  }

  handleClick() {
    console.log('demo');
  }
  
  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

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

使用属性初始化语法:

//代码4
class MyComponent extends React.Component {

  handleClick = () => console.log('demo');
  
  render() {
    return <button onClick={this.handleClick}>Click</button>;
  }
}

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

事件响应函数的传参问题

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

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

//代码5
class MyComponent extends React.Component {
  handleClick = (props, e) => {
    console.log(props, e);
  };

  render() {
    return <button onClick={e => this.handleClick("demo", e)}>点击</button>;
  }
}

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

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

//代码6
class MyComponent extends React.Component {
  handleClick(props) {
    console.log(props)
  }

  render() {
    return <button onClick={this.handleClick.bind(this, 'demo')}>点击</button>
}

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

//代码7
class MyComponent extends React.Component {
  handleClick = (props) => console.log(props)

  render() {
    return <button onClick={this.handleClick.bind(undefined, 'demo')}>点击</button>
}

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

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

小结

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

合成事件

SyntheticEvent 实例将被传递给你的事件处理函数,它是浏览器的原生事件的跨浏览器包装器。除兼容所有浏览器外,它还拥有和浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。

如果因为某些原因,当你需要使用浏览器的底层事件时,只需要使用 nativeEvent 属性来获取即可。每个 SyntheticEvent 对象都包含以下属性:

  • boolean bubbles => 事件是否可冒泡
  • boolean cancelable => 事件是否可拥有取消的默认动作
  • DOMEventTarget currentTarget => 事件监听器触发该事件的元素(绑定事件的元素)
  • boolean defaultPrevented => 当前事件是否调用了 event.preventDefault()方法
  • number eventPhase => 事件传播的所处阶段[0:Event.NONE-没有事件被处理,1:Event.CAPTURING_PHASE - 捕获阶段,2:被目标元素处理,3:冒泡阶段(Event.bubbles为true时才会发生)]
  • boolean isTrusted => 触发是否来自于用户行为,false为脚本触发
  • DOMEvent nativeEvent => 浏览器原生事件
  • void preventDefault() => 阻止事件默认行为
  • boolean isDefaultPrevented() => 返回的事件对象上是否调用了preventDefault()方法
  • void stopPropagation() => 阻止事件冒泡
  • boolean isPropagationStopped() => 返回的事件对象上是否调用了stopPropagation()方法
  • DOMEventTarget target => 触发事件的元素
  • number timeStamp => 事件生成的日期和时间
  • string type => 当前 Event 对象表示的事件的名称,是注册事件的句柄,如,click、mouseover...

事件池

SyntheticEvent 是合并而来。这意味着 SyntheticEvent 对象可能会被重用,而且在事件回调函数被调用后,所有的属性都会无效。出于性能考虑,你不能通过异步访问事件。

function onClick(event) {
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => null
    console.log(eventType); // => "click"
  }, 0);

  // 不起作用,this.state.clickEvent 的值将会只包含 null
  this.setState({clickEvent: event});

  // 你仍然可以导出事件属性
  this.setState({eventType: event.type});
}

注意:如果你想异步访问事件属性,你需在事件上调用 event.persist(),此方法会从池中移除合成事件,允许用户代码保留对事件的引用。

function onClick(event) {
  event.persist(); // => 从池中移除合成事件
  
  console.log(event); // => nullified object.
  console.log(event.type); // => "click"
  const eventType = event.type; // => "click"

  setTimeout(function() {
    console.log(event.type); // => "click"
    console.log(eventType); // => "click"
  }, 0);

  // 起作用
  this.setState({clickEvent: event});

  // 你仍然可以导出事件属性
  this.setState({eventType: event.type});
}

事件委托的机制

因为提到事件处理,那我们也看下事件委托机制吧,感兴趣的可以了解下

首先我们来看下事件捕获和事件冒泡:

事件捕获:当某个元素触发某个事件(如onclick),顶层对象document就会发出一个事件流,随着DOM树的节点向目标元素节点流去,直到到达事件真正发生的目标元素。在这个过程中,事件相应的监听函数是不会被触发的。

事件目标:当到达目标元素之后,执行目标元素该事件相应的处理函数。如果没有绑定监听函数,那就不执行。

事件冒泡:从目标元素开始,往顶层元素传播。途中如果有节点绑定了相应的事件处理函数,这些函数都会被一次触发。如果想阻止事件起泡,可以使用e.stopPropagation()或者e.cancelBubble=true(IE)来组织事件的冒泡传播。

image

事件代理:当子节点被点击时,click事件向上冒泡,父节点捕获到事件后,我们判断是否为所需的节点,然后进行处理。

这就是事件委托机制的模型

好了,关于事件处理和合成事件就介绍到这里了。欢迎交流!