React基于Virtual DOM实现了一个合成事件层,我们所定义的时间处理器会接受到一个合成事件对象的实例,它完全符合W3C标准,因此不会存在兼容性问题。
同样支持事件的冒泡机制,所有的事件都自动绑定到最外层上(document)。如果需要访问原生事件对象,可以使用nativeEvent
对象。
合成事件的绑定方式
在DOM0级事件中,事件处理器是直接绑定到HTML元素之上,例如:
<div onclick="clickHandler()">Click</div>
而React借鉴了这种写法:
<button onClick={this.clickHandle}>Click</button>
但也仅仅是写法相近,在JSX中需要使用驼峰命名法的形式来书写事件的属性名,例如上面的onClick
,此外在HTML中,属性值只能是字符串,而在JSX中可以是任意类型。
合成事件的实现机制
在React底层主要对合成事件做了两件事:事件委派和自动绑定。
事件委托
在React中,事件处理函数并不是直接绑定到真正的节点上,而是把所有事件绑定到结构的最外层。使用一个统一的事件监听器,这个事件监听器维持了一个映射来保存所有组件内部的事件监听和处理函数。
当事件发生时首先被这个统一的事件监听器处理,然后在映射里找到真正的事件处理函数并调用。
自动绑定
在React组件中,每个方法的上下文都会指向该组件的实例,即自动绑定this
为当前组件,而且React还会对这种引用进行缓存。
但是在用ES6的class写法或纯函数写法时,这种自动绑定就不存在了,需要手动绑定。
bind
方法:
import React, {Component} from 'react';
class App extends Component{
constructor(props){
super(props);
}
clickHandle(e){
console.log(e);
}
render(){
return (
<div ref="div">
<button onClick={this.clickHandle.bind(this)}>Click Me</button>
</div>
)
}
}
如果方法只绑定,不传参,那还有一个更便捷的方法–双冒号语法。即:
import React, {Component} from 'react';
class App extends Component{
constructor(props){
super(props);
}
clickHandle(e){
console.log(e);
}
render(){
return (
<div ref="div">
<button onClick={::this.clickHandle}>Click Me</button>
</div>
)
}
}
- 构造器声明:
import React, {Component} from 'react';
class App extends Component{
constructor(props){
super(props);
this.clickHandle = this.clickHandle.bind(this);
}
clickHandle(e){
console.log(e);
}
render(){
return (
<div ref="div">
<button onClick={this.clickHandle}>Click Me</button>
</div>
)
}
}
这样写的好处在于仅需要进行一次绑定,而不需要每次调用事件处理函数的时候都执行一次绑定操作。
在React中使用原生事件
除了合成事件,在React中也可以使用原生事件,在componentDidMount
生命周期中,组件已经完成挂载并且在浏览器中存在真实的DOM。
import React, {Component} from 'react';
import './App.css';
class App extends Component{
constructor(props){
super(props);
this.clickHandle = this.clickHandle.bind(this);
}
clickHandle(e){
console.log('子元素');
}
componentDidMount(){
const div = this.refs.div;
div.addEventListener('click', (e)=>{
if(e.target.tagName === 'BUTTON'){
console.log('不执行操作');
return;
}
});
}
componentWillUnmount(){
const div = this.refs.div;
div.removeEventListener('click');
}
render(){
return (
<div ref="div">
<button onClick={this.clickHandle}>Click Me</button>
</div>
)
}
}
使用DOM原生事件时,一定要在组件卸载时手动移除,否则可能出现内存泄漏的问题。
原生事件和合成事件的混用
import React, {Component} from 'react';
import './App.css';
class App extends Component{
constructor(props){
super(props);
this.state = {
inputState: ''
}
this.clickHandle = this.clickHandle.bind(this);
}
clickHandle(e){
console.log('子元素');
}
componentDidMount(){
const div = this.refs.div;
div.addEventListener('click', (e)=>{
console.log('父级元素');
});
}
componentWillUnmount(){
const div = this.refs.div;
div.removeEventListener('click');
}
render(){
// const {inputState} = this.state;
return (
<div ref="div">
<button onClick={this.clickHandle}>Click Me</button>
</div>
)
}
}
点击按钮之后,控制台输出结果如下:
可以看到,子元素和父元素的事件处理函数都被触发了。一般来说,我们会在子元素的事件处理函数内执行stopPropagation
来阻止事件的冒泡。不过这在React中是不起作用的。
首先,对于合成事件来说,stopPropagation
只能阻止合成事件的冒泡,而不能阻止原生事件。
可能我们马上会想到调用合成事件对象的nativeEvent
的stopPropagation
不就可以阻止原生事件的冒泡了吗?的确,理论上应该是可以的,然而因为React将合成事件的事件处理函数绑定在了最外层(document)上,所以等到最外层再执行也已是为时已晚。
对于这种情况,应该通过判别事件的target
对象来避免。
class App extends Component{
constructor(props){
super(props);
this.state = {
inputState: ''
}
this.clickHandle = this.clickHandle.bind(this);
}
clickHandle(e){
console.log('子元素');
}
componentDidMount(){
const div = this.refs.div;
div.addEventListener('click', (e)=>{
if(e.target.tagName === 'BUTTON'){
console.log('不执行操作');
return;
}
});
}
componentWillUnmount(){
const div = this.refs.div;
div.removeEventListener('click');
}
render(){
return (
<div ref="div">
<button onClick={this.clickHandle}>Click Me</button>
</div>
)
}
}