淺析React之事件系統(二)

aaronisme發表於2017-02-05

上篇文章中,我們談到了React事件系統的實現方式,和在React中使用原生事件的方法,那麼這篇文章我們來繼續分析下,看看React中合成事件和原生事件混用的各種情況。

上一個例子

在上篇文章中,我們舉了個例子。為了防止大家不記得,我們來看看那個例子的程式碼。

class App extends React.Component {

  constructor(props){
    super(props);
    
    this.state = {
      show: false
    }
    
    this.handleClick = this.handleClick.bind(this)
    this.handleClickImage = this.handleClickImage.bind(this);
  }
  
  handleClick(){
   this.setState({
     show: true
   })
  }
  
  componentDidMount(){
    document.body.addEventListener(`click`, e=> {
      this.setState({
        show: false
      })
    })
  }
  
  componentWillUnmount(){
    document.body.removeEventListener(`click`);
  }
  
  handleClickImage(e){
    console.log(`in this `)
    e.stopPropagation();
  }
  
  render(){
    return (
      <div className="container">
        <button onClick={this.handleClick}>Open Image</button>
          <div className="img-container" style={{ display: this.state.show ? `block`: `none`}} onClick={this.handleClickImage}>
            <img src="http://blog.addthiscdn.com/wp-content/uploads/2014/11/addthis-react-flux-javascript-scaling.png" />
          </div>
      </div>
    )
  }
}
ReactDOM.render(<App />, document.getElementById(`root`));

這有什麼問題呢? 問題就在於,如果我們點選image的內部依舊可以收起Image,那麼這是為什麼呢?這是因為我們及時點選了Image的內部,body上繫結的事件處理器依舊會執行,這樣就讓我們的image收起來了。那我們如果不想讓image收起來改怎麼做呢?

首先的想法是停止冒泡,如果我們在img-container中就停止冒泡了是不是就可以讓image不消失了呢?比如這樣:

...
handleClickImage(e){
    e.preventDefault();
    e.stopPropagation();
  }
  
  render(){
    return (
      <div className="container">
        <button onClick={this.handleClick}>Open Image</button>
          <div className="img-container" style={{ display: this.state.show ? `block`: `none`}} onClick={this.handleClickImage}>
            <img src="http://blog.addthiscdn.com/wp-content/uploads/2014/11/addthis-react-flux-javascript-scaling.png" />
          </div>
      </div>
    )
  }
...

Open In CodePen

在這裡我們定義一個handleClickImage的方法,在其中我們執行取消預設行為和停止冒泡。那是似乎效果並不是我們想要的。因為阻止React事件冒泡的行為只能用於React合成事件中,沒法阻止原生事件的冒泡。同樣用React.NativeEvent.stopPropagation()也是無法阻止冒泡的。

如何解決這樣的問題呢?首先,儘量的避免混用合成事件和原生事件。需要注意的點是:

  1. 阻止react 合成事件冒泡並不會阻止原生時間的冒泡,從上邊的例子我們已經看到了,及時使用stopPropagation也是無法阻止原生時間的冒泡的。

  2. 第二點需要注意的是,取消原生時間的冒泡會同時取消React Event。並且原生事件的冒泡在react event的觸發和冒泡之前。同時React Event的建立和冒泡是在原生事件冒泡到最頂層的component之後的。我們來看這個例子:

class App extends React.Component {
  
  render(){
   return <GrandPa />;
  }
}

class GrandPa extends React.Component {
  constructor(props){
      super(props);
      this.state = {clickTime: 0};
      this.handleClick = this.handleClick.bind(this);
  }
  
 handleClick(){
   console.log(`React Event grandpa is fired`);
  this.setState({clickTime: new Date().getTime()})
};
  
  componentDidMount(){
    document.getElementById(`grandpa`).addEventListener(`click`,function(e){
      console.log(`native Event GrandPa is fired`);
    })
  }
  
  render(){
    return (
      <div id=`grandpa` onClick={this.handleClick}>
        <p>GrandPa Clicked at: {this.state.clickTime}</p>
        <Dad />
      </div>
    )
  }
}

class Dad extends React.Component {
  constructor(props){
    super(props);
    this.state = {clickTime:0};
    this.handleClick=this.handleClick.bind(this);
  }
  
  componentDidMount(){
    document.getElementById(`dad`).addEventListener(`click`,function(e){
      console.log(`native Event Dad is fired`);
      e.stopPropagation();
    })
  }
  
  handleClick(){
    console.log(`React Event Dad is fired`)
    this.setState({clickTime: new Date().getTime()})
  }
  
  render(){
    return (
      <div id=`dad` onClick={this.handleClick}>
       <p>Dad Clicked at: {this.state.clickTime}</p>
        <Son />
      </div>
     )
  }
}

class Son extends React.Component {
  constructor(props){
    super(props);
    this.state = {clickTime:0};
    this.handleClick=this.handleClick.bind(this);
  }
  
  handleClick(){
    console.log(`React Event Son is fired`);
    this.setState({clickTime: new Date().getTime()})
  }
  
  componentDidMount(){
    document.getElementById(`son`).addEventListener(`click`,function(e){
      console.log(`native Event son is fired`);
    })
  }
  
  render(){
    return (
      <div id="son">
       <p onClick={this.handleClick}>Son Clicked at: {this.state.clickTime} </p>
      </div>
     )
  }
}

ReactDOM.render(<App />, document.getElementById(`root`));

Open in CodePen

在這個例子中我們有三個component,Son Dad,Grandpa。同時定義了React Event handler 和 native event handler,並在Dad的native Event handler中stopPropagation,當我們點選Son or Dad component的時候會發現,React Event handler並沒有被trigger。
console裡的output為:

"native Event son is fired"
"native Event Dad is fired"

這就說明native Event的停止冒泡可以阻斷所有的React Event。所以即使我們是在Dad上停止冒泡的,依舊阻斷了Son上的React Event。

同時如果我們把dad上的stopPropagation remove掉我們會看到如下結果:

"native Event son is fired"
"native Event Dad is fired"
"native Event GrandPa is fired"
"React Event Son is fired"
"React Event Dad is fired"
"React Event grandpa is fired"

這就說明React的合成時間是在原生事件冒泡到最頂層元件結束後才建立和冒泡的,也是符合React的原理,因為在是實現的時候React只是將一個Event listener 掛在了最頂層的元件上,其內部一套自己的機制進行事件的管理。

淺析React之事件系統(一)

相關文章