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>
)
}
}