React合成事件系統(tǒng)

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、事件綁定的三種方式

  1. 組件上綁定
<Component 事件={this.方法.bind(this)}></Component>

缺點(diǎn):每次點(diǎn)擊時(shí)都需要重新綁定一個(gè)函數(shù),所以這種方式對(duì)性能有一定影響(因?yàn)楹瘮?shù)的創(chuàng)建和銷(xiāo)毀都是需要開(kāi)銷(xiāo)的)。

  1. 構(gòu)造方法中綁定
constructor(props){
    super(props) ;
    this.方法 = this.方法.bind(this,'event','args') ;
    // event(事件名) 和 args(參數(shù)) 不是必須的。
}
<Component 事件={this.方法}></Component>

優(yōu)點(diǎn):只需要在組件初始化時(shí)綁定一次即可。

  1. 使用箭頭函數(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 就是事件冒泡.

事件傳播是可以阻止的:

  1. 禁止事件冒泡
W3C: e.topPropagation();
IE:  window.event.cancelBubble = true;
  1. 禁止默認(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)有用。

解決方法:

  1. 使用原生事件來(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>

  1. 通過(guò)e.target判斷
componentDidMount(){
  document.body.addEventListener('click', e=>{
    if(e.target &&  e.target.matches('div.image')){
      return;
    }
    
    this.setState({
      visible: false,
    });
  });
}

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 1 React合成事件特點(diǎn) React自己實(shí)現(xiàn)了一套高效的事件注冊(cè),存儲(chǔ),分發(fā)和重用邏輯,在DOM事件體系基礎(chǔ)上做...
    Dabao123閱讀 2,121評(píng)論 1 1
  • 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。 PS:轉(zhuǎn)載請(qǐng)注明出處作者:TigerChain地址:http...
    TigerChain閱讀 8,471評(píng)論 1 9
  • 原教程內(nèi)容詳見(jiàn)精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過(guò)程中的一些閱讀筆記,個(gè)人覺(jué)得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,942評(píng)論 1 18
  • 本文通過(guò)一個(gè)簡(jiǎn)短的實(shí)例&控制臺(tái)調(diào)試,了解react事件處理的全過(guò)程。下面是測(cè)試用代碼,使用控制臺(tái)可以清晰看到函數(shù)執(zhí)...
    溪離欣洛閱讀 1,282評(píng)論 2 2
  • ??JavaScript 與 HTML 之間的交互是通過(guò)事件實(shí)現(xiàn)的。 ??事件,就是文檔或?yàn)g覽器窗口中發(fā)生的一些特...
    霜天曉閱讀 3,687評(píng)論 1 11

友情鏈接更多精彩內(nèi)容