React基于虛擬DOM實(shí)現(xiàn)了一個(gè)合成事件層,我們所定義的事件處理器會(huì)接收到一個(gè)合成事件對(duì)象的實(shí)例,它完全符合 W3C 標(biāo)準(zhǔn),不會(huì)存在任何IE標(biāo)準(zhǔn)的兼容問(wèn)題。并且與原生瀏覽器具有一樣的接口,同樣支持事件冒泡機(jī)制,可用 stopPropagation() 和 preventDefault() 來(lái)中斷它。
所有事件都自動(dòng)綁定在最外層上。如果需要訪問(wèn)原生事件對(duì)象,可以使用nativeEvent屬性。
1、綁定方式
React事件的綁定方式在寫(xiě)法上與原生HTML事件監(jiān)聽(tīng)很相似。
<button onClick={this.handleClick}></button>
但有以下區(qū)別:
(1)JSX中采用駝峰式屬性命名方式,HTML事件則是全部小寫(xiě):onclick
(2)JSX中props的值可以是任意類(lèi)型,但 HTML 中只能是字符串:onclick="handleClick()"
(3)React事件沒(méi)有直接綁定在 HTML 元素上!只是借鑒了這種寫(xiě)法。React中所有事件使用了事件委托方式自動(dòng)綁定在最外層的。
2、實(shí)現(xiàn)機(jī)制
1、事件委派
React并不會(huì)把事件處理函數(shù)直接綁定到真實(shí)的節(jié)點(diǎn)上,而是使用一個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器 ReactEventListener ,把所有事件綁定到結(jié)構(gòu)的最外層 document 節(jié)點(diǎn)上,依賴(lài)冒泡機(jī)制完成了事件委派。
ReactEventListener:維持了一個(gè)映射來(lái)保存所有組件內(nèi)部的事件監(jiān)聽(tīng)和處理函數(shù),負(fù)責(zé)事件注冊(cè)和事件分發(fā)。當(dāng)組件在掛載或卸載時(shí),只是在這個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器上插入或刪除一些對(duì)象;當(dāng)事件發(fā)生時(shí),首先被這個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器處理,然后在映射里找到真正的事件處理函數(shù)并調(diào)用。這樣簡(jiǎn)化了事件處理和回收機(jī)制,提升了效率。
- 事件注冊(cè)包括兩方面:(1)將時(shí)間事件注冊(cè)到 document 這個(gè)原生DOM上(2)將注冊(cè)的事件采用事務(wù)隊(duì)列的當(dāng)時(shí)存儲(chǔ)起來(lái),以便事件觸發(fā)時(shí)回調(diào)。
- 事件分發(fā):通過(guò) ReactEventListener.dispatchEvent 回調(diào)函數(shù)實(shí)現(xiàn)。當(dāng)事件觸發(fā)時(shí),document上 addEventListener 注冊(cè)的 callback 會(huì)被回調(diào),回調(diào)函數(shù)就是 ReactEventListener.dispatchEvent ,dispatchEvent會(huì)找到事件觸發(fā)的 DOM 及其對(duì)應(yīng)的 React 組件。
ReactEventEmitter:負(fù)責(zé)每個(gè)組件上事件的執(zhí)行。事件的執(zhí)行采用冒泡機(jī)制,從觸發(fā)事件的對(duì)象開(kāi)始,向父元素回溯,依次調(diào)用它們注冊(cè)的事件callback。
2、事件綁定的三種方式
- 組件上綁定
<Component 事件={this.方法.bind(this)}></Component>
缺點(diǎn):每次點(diǎn)擊時(shí)都需要重新綁定一個(gè)函數(shù),所以這種方式對(duì)性能有一定影響(因?yàn)楹瘮?shù)的創(chuàng)建和銷(xiāo)毀都是需要開(kāi)銷(xiāo)的)。
- 構(gòu)造方法中綁定
constructor(props){
super(props) ;
this.方法 = this.方法.bind(this,'event','args') ;
// event(事件名) 和 args(參數(shù)) 不是必須的。
}
<Component 事件={this.方法}></Component>
優(yōu)點(diǎn):只需要在組件初始化時(shí)綁定一次即可。
- 使用箭頭函數(shù)
<Component 事件={(e) => this.方法(e,args)}</Component>
//其中 args (參數(shù))不是必須的
缺點(diǎn):由于箭頭函數(shù)綁定是定義在 redner 方法中的,故組件每一次渲染都會(huì)創(chuàng)建一個(gè)新的箭頭函數(shù)。同第一種方式,對(duì)性能有影響。
3、在React中使用原生事件
有時(shí)候React合成事件并不能滿(mǎn)足我們的需求,我們不得不使用原生事件。原生事件就是我們?cè)诮M件掛載后,在 componentDidMount 或 componentDidUpdate 方法中通過(guò) ddEventListener 綁定的事件。
原生事件的傳播方式有兩種:事件冒泡和事件捕獲。事件冒泡是從內(nèi)層元素向外層元素觸發(fā),事件捕獲是從外層元素向內(nèi)層元素觸發(fā)。具體采用哪一種傳播方式,可以通過(guò)方法 addEventListener 的第三個(gè)參數(shù)來(lái)指定。為 true 就是事件捕獲,為 false 就是事件冒泡.
事件傳播是可以阻止的:
- 禁止事件冒泡
W3C: e.topPropagation();
IE: window.event.cancelBubble = true;
- 禁止默認(rèn)事件
W3C: e.preventDefault();
IE: window.event.returnValue = false;
注意:
- 合成事件解決了IE兼容性問(wèn)題,使用stopPropagation()即可禁止事件冒泡。
- 阻止 React 合成事件冒泡,并不能阻止原生事件的冒泡,就算使用 stopPropagation 也無(wú)法阻止原生事件的冒泡。
- 取消原生事件的冒泡也會(huì)同時(shí)取消 React 事件,并且原生事件的冒泡在 React 事件的觸發(fā)和冒泡之前。
對(duì)于原生事件,一定要手動(dòng)移除,否則很可能出現(xiàn)內(nèi)存泄漏的問(wèn)題。一般在componentDidMount() 方法中注冊(cè)原生事件,在 componentWillUnmount() 方法中移除事件。
一般來(lái)說(shuō),盡量不要混用合成事件和原生事件,只有在使用 React 合成事件無(wú)法解決問(wèn)題的這一場(chǎng)景,才去使用原生事件。因?yàn)閮煞N混用非常容易導(dǎo)致問(wèn)題,例如這樣一個(gè)功能:點(diǎn)擊按鈕顯示圖片,點(diǎn)擊非圖片區(qū)域時(shí)將其隱藏。代碼如下:
constructor(){
this.handleClick = this.handleClick.biand(this);
this.handleClickImg = this.handleClickImg.biand(this);
this.state={
visible: false,
};
}
componentDidMount(){
// 點(diǎn)擊頁(yè)面隱藏圖片
document.body.addEventListener('click', e=>{
this.setState({
visible: false,
});
});
}
componentWillUnmount(){
document.body.removeEventListener('click'); // 組件卸載時(shí),手動(dòng)移除原生事件
}
// 點(diǎn)擊按鈕顯示圖片
handleClick(){
this.setState({
visible: true,
});
}
handleClickImg(){
e.stopPropagation(); // 試圖實(shí)現(xiàn):點(diǎn)擊圖片時(shí),不隱藏圖片
}
render(){
return (
<div>
<button onClick={this.handleClick} >點(diǎn)擊顯示圖片</button>
<div
className="image"
style={{display: this.state.visible ? 'block' : 'none'}}
onClick={this.handleClickImg}
>
<img src='' alt=''>
</div>
</div>
)
}
在上面這個(gè)例子中,預(yù)期結(jié)果是:點(diǎn)擊圖片區(qū)域,圖片不會(huì)隱藏。然而實(shí)際效果是點(diǎn)擊圖片時(shí)依然會(huì)隱藏圖片。這就是因?yàn)?React 合成事件的委托機(jī)制的原因:在合成事件內(nèi)部?jī)H僅對(duì)最外層容器進(jìn)行了綁定,并且依賴(lài)事件冒泡機(jī)制完成了委派。也就是說(shuō),handleClickImg事件并沒(méi)有直接綁定在div.image元素上,因此在此處使用 e.stopPropagation() 并沒(méi)有用。
解決方法:
- 使用原生事件來(lái)阻止
document.querySelector('image').addEventListener('click', e=>{
e.stopPropagation();
});
componentWillUnmount(){
document.body.removeEventListener('click'); // 組件卸載時(shí),手動(dòng)移除原生事件
document.querySelector('image').removeEventListener('click');
}
<div
className="image"
style={{display: this.state.visible ? 'block' : 'none'}}
>
<img src='' alt=''>
</div>
- 通過(guò)e.target判斷
componentDidMount(){
document.body.addEventListener('click', e=>{
if(e.target && e.target.matches('div.image')){
return;
}
this.setState({
visible: false,
});
});
}