玩轉 React(六)- 處理事件

前面的文章介紹了 React 的 JSX 語法、組件的建立方式、組件的屬性、組件的內部狀態以及組件的生命週期。另外,還順帶說了各個知識點要重點注意的事情,以及我在項目實踐中的一些經驗。若是你以爲對本身有幫助,能夠經過 玩轉 React(一)- 前言 中的文章目錄進行閱讀。javascript

另外,爲了方便你們更好地交流 React、分享前端開發經驗,我建了一個微信羣,因爲微信羣二維碼有時間限制,你能夠先加我好友(個人微信:leobaba88),驗證信息 玩轉 React,我會拉你入羣,歡迎你們,下面是個人微信二維碼。html

好的,言歸正傳,今天咱們說一下在 React 中是如何處理事件的。事件處理是前端開發過程當中很是重要的一部分,經過事件處理機制,咱們的前端應用能夠響應用戶的各類操做,從而實現一個富交互的前端應用。前端

內容摘要

  • 如何爲 React 的內置組件設置事件處理函數。java

  • React 事件對象與瀏覽器原生 DOM 事件對象的區別。react

  • 默認狀況下不能以異步的方式使用事件對象,如在 setTimeout 中。git

  • 不要在組件中使用 addEventListener 註冊事件處理函數,有坑。github

  • 綁定事件處理函數 this 指向的四中方式以及他們的優缺點。segmentfault

React 內置組件的事件處理

我所說的 React 內置組件是指 React 中已經定義好的,能夠直接使用的如 div、button、input 等與原生 HTML 標籤對應的組件。瀏覽器

咱們先回顧一下瀏覽器原生 DOM 上註冊事件的方式。微信

第一種方式

<a href="#" onclick="console.info('You clicked me.'); return false;">
    Click me.
</a>

這是一種古老的方式,在 DOM level 1 規範中的事件註冊方式,如今已經不多使用了。

這種方式,用來註冊事件的 HTML 屬性的值是一個字符串,是一段須要執行的 JavaScript 代碼。

能夠經過 return false; 來阻止當前 HMTL 元素的默認行爲,如 a 標籤的頁面跳轉。

關於 DOM 規範的級別能夠參考:DOM Levels

第二種方式:

<a href="#" id="my-link">
    Click me.
</a>

<script type="text/javascript">
    document.querySelector('#my-link').addEventListener('click', (e) => {
        e.preventDefault();
        console.info("You clicked me.");
    });
</script>

這是 DOM level 2 規範中引入的事件註冊方式,目前各瀏覽器也支持的很好,用得是最多的,就是寫起來有點囉嗦哈。

在 React 中,事件註冊與方式一很是相似,不過有以下幾點不一樣:

  • 屬性名稱採用駝峯式(如:onClick,onKeyDown),而不是全小寫字母。

  • 屬性值接受一個函數,而不是字符串。

  • return false; 不會阻止組件的默認行爲,須要調用 e.preventDefault();

以下所示:

function ActionLink() {
  function handleClick(e) {
    e.preventDefault();
    console.log('The link was clicked.');
  }

  return (
    <a href="#" onClick={handleClick}>
      Click me
    </a>
  );
}

這是一個以函數方式定義的組件,組件渲染一個 a 元素,設置l連接的點擊事件,經過事件處理函數接收到的事件對象(e),阻止了連接的默認行爲,並打印 "The link was clicked." 到控制檯上。設置 React 內置組件的事件處理函數是否是很是簡單。

React 事件對象 VS 原生的 DOM 事件對象

React 中的事件對象稱之爲 SyntheticEvent(合成對象),它是依據 DOM Level 3 的事件規範實現的,這樣作最大的好處是能夠屏蔽瀏覽器的差別,各類廠商的瀏覽器對規範的實現程度是不同的,若是直接使用原生 DOM 事件對象的話,有些狀況下你須要考慮瀏覽器的兼容性。而 React 經過 SyntheticEvent 已經把這些雜事幫你搞定了,在任何 React 支持的瀏覽器下,事件對象都有一致的接口。

React 中全部的事件處理函數都會接收到一個 SyntheticEvent 的實例 e 做爲參數,若是在某些特殊的場景中,你須要用到原生的 DOM 事件對象,能夠經過 e.nativeEvent 來獲取。

不要在異步過程當中使用 React 事件對象

須要說明的是,出於性能的考慮,React 並非爲每個事件處理函數生成一個全新的事件對象,事件對象會被複用,當事件處理函數被執行之後,事件對象的全部屬性會被設置爲 null,因此在事件處理函數中,你不能以異步的方式使用 React 的事件對象,由於那時候事件對象的全部屬性都是 null 了,或者已經不是你關心的那個事件了。

儘可能不要使用 addEventListener

這裏稍微深刻一下,否則我怕有的同窗會踩坑。React 內部本身實現了一套高效的事件機制,爲了提升框架的性能,React 經過 DOM 事件冒泡,只在 document 節點上註冊原生的 DOM 事件,React 內部本身管理全部組件的事件處理函數,以及事件的冒泡、捕獲。

因此說,若是你經過 addEventListener 註冊了某個 DOM 節點的某事件處理函數,而且經過 e.stopPropagation(); 阻斷了事件的冒泡或者捕獲,那麼該節點下的全部節點上,同類型的 React 事件處理函數都會失效。

以下示例,雖然設置的連接的點擊事件,可是它卻執行不了。

class CounterLink extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    this.handleClick = this.handleClick.bind(this);
  }
  componentDidMount() {
    document.querySelector('.my-link').addEventListener('click', (e) => {
      console.info('raw click');
      e.stopPropagation();
    })
  }
  handleClick(e) {
    e.preventDefault();
    console.info('react click');
    this.setState({ count: this.state.count + 1 });
  }
  render() {
    return (
      <div className="my-link">
        <a href="#" onClick={this.handleClick}>Clicked me {this.state.count} times.</a>    
      </div>
    )
  }
}
ReactDOM.render(<CounterLink/>, document.querySelector("#root"));

https://codepen.io/Sarike/pen...

如何綁定事件處理函數的 this

在以類繼承的方式定義的組件中,爲了能方便地調用當前組件的其餘成員方法或屬性(如:this.state),一般須要將事件處理函數運行時的 this 指向當前組件實例。

以下面的示例:

class Link extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }
  handleClick(e) {
    e.preventDefault();
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return <a href="#" onClick={this.handleClick}>Clicked me {this.state.count} times.</a>    
  }

}

ReactDOM.render(<Link/>, document.querySelector("#root"))

當點擊連接時,控制檯會報錯:Uncaught TypeError: Cannot read property 'setState' of undefined,就是由於沒有將 handleClick 運行時的 this 綁定到當前組件。

綁定事件處理函數的 this 到當前組件,有以下幾種方式。

第一種方式,經過 bind 方法,原地綁定事件處理函數的 this 指向,以下所示:

<a href="#" onClick={this.handleClick.bind(this)}>
    Clicked me {this.state.count} times.
</a>

這種方式的優勢是書寫起來相對簡單,可是每次渲染都會執行 bind 方法生成一個新的函數,會有額外的開銷,因爲事件處理函數是做爲屬性傳遞的,因此從而致使子組件進行從新渲染,顯然這不是一種好的方式。

第二種方式,經過一個箭頭函數將真實的事件處理函數包裝一下,以下所示:

<a href="#" onClick={e => this.handleClick(e)}>
    Clicked me {this.state.count} times.
</a>

這種方式書寫起來也不算麻煩,不過也沒有解決第一種方式面臨的性能開銷和從新渲染的問題。可是這種方式的一個好處是能清晰描述事件處理函數接收的參數列表(這一點可能因人而異,我的觀點以爲這是一個優勢)。

第三種方式,在 constructor 中預先將全部的事件處理函數經過 bind 方法進行綁定。以下所示:

class Link extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
    
    // 重點在這裏
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick(e) {
    e.preventDefault();
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return <a href="#" onClick={this.handleClick}>Clicked me {this.state.count} times.</a>    
  }
}

ReactDOM.render(<Link/>, document.querySelector("#root"))

這種方式能解決前兩種方式面臨的額外開銷和從新渲染的問題,可是寫起來略微有點複雜,由於一個事件處理函數要分別在三個不一樣的地方進行定義、綁定 this 和使用。

第四種方式,使用類的成員字段定義語法,以下所示:

class Link extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    }
  }
  handleClick = e => {
    e.preventDefault();
    this.setState({ count: this.state.count + 1 })
  }
  render() {
    return <a href="#" onClick={this.handleClick}>Clicked me {this.state.count} times.</a>    
  }
}
ReactDOM.render(<Link/>, document.querySelector("#root"))

這種方式解決了上面三種方式面臨的性能開銷、從新渲染以及書寫麻煩的問題。惟一的問題就是這種語法目前處於 Stage 3,還未歸入到正式的 ES 規範中。參考:https://github.com/tc39/propo...

不過這也沒太大關係。

總結

本文的內容並很少,可能說的有點囉嗦。簡單總結一下,React 中經過設置組件的 事件屬性 來註冊事件,React 內部本身實現了一套包含冒泡、捕獲邏輯在內的事件機制,因此儘可能不要使用 addEventListener,除非你知道本身在幹什麼。有四種爲事件處理函數綁定 this 的方法,推薦使用類屬性定義的方式來定義處理函數,若是你不太在乎哪一點性能開銷的話,可使用箭頭函數包裝真實事件回調的方式。另外,事件對象在 React 中是被複用的,事件回調被執行之後,事件對象的全部屬性會被重置爲 null,因此不要在異步的過程當中使用事件對象。

好了,有什麼疑問能夠加微信羣交流,個人微信號:leobaba88,驗證信息:玩轉 React。

相關文章
相關標籤/搜索