React躬行記(6)——事件
React在原生事件的基礎上,重新設計了一套跨瀏覽器的合成事件(SyntheticEvent),在事件傳播、註冊方式、事件物件等多個方面都做了特別的處理。
一、註冊事件
合成事件採用宣告式的註冊方式,類似於設定HTML屬性,但有兩點不同:
(1)事件要採用小駝峰的命名法,而不是全部小寫,例如onclick要寫成onClick。
(2)在JSX中的事件處理程式是一個函式引用,而不是一段字串,如下程式碼所示,其中handle()是一個函式。
<button onclick="handle()">提交</button> <button onClick={handle}>提交</button>
兩個<button>元素都註冊了點選事件,其中前者採用了傳統的事件註冊,後者使用了合成事件,通過對比可發現它們之間有兩處明顯的不同。
React的事件型別是原生事件型別的一個子集,在上面的示例中,為按鈕添加了Mouse型別的onClick事件。除了常規的Focus、Form、Touch等型別之外,React還支援Selection、Wheel、Media等有特殊功能的事件型別。
二、事件傳播
React中的事件最終都會轉換成原生事件,但不會註冊到某個真實的DOM元素,而是繫結到根元素,使用事件委託技術進行事件代理,再由一個總的事件監聽器管理所有的事件處理程式。
React中的事件傳播只有冒泡這一種形式,而冒泡會先在DOM樹中向上傳播,一直到根元素,然後再從JSX中觸發的React元素開始向它的父元素傳播,依次執行繫結的事件處理程式。在下面的示例中,分別為頁面中的根元素和<body>元素,以及元件中的<div>元素和<button>元素綁定了點選事件,其中<div>是<button>的父元素。
document.documentElement.addEventListener("click", function(e) { console.log("html"); }); document.body.addEventListener("click", function(e) { console.log("body"); }); class Btn extends React.Component { handle1(e) { console.log("子元素"); } handle2(e) { console.log("父元素"); } render() { return ( <div onClick={this.handle2}> <button onClick={this.handle1}>提交</button> </div> ); } }
當點選提交按鈕時,在控制檯輸出的順序是“body”、“html”、“子元素”、“父元素”。在React的合成事件中,也會包含一個事件物件(例如handle1()函式中的e引數)。如果要停止冒泡傳播,那麼可以在相應的事件處理程式中呼叫e.stopPropagation()。
三、事件物件
React基於W3C規範對事件物件進行了一層封裝,不僅保證了瀏覽器的相容性,還提供了一個訪問原生事件物件的入口(即呼叫自身的nativeEvent屬性)。注意,在合成事件中阻止元素的預設行為,不能用返回false的方式,得呼叫事件物件的preventDefault()方法。
合成事件是被池化的,在執行完事件處理程式後,其引數(即事件物件)的屬性將會被丟棄,例如通過定時器非同步呼叫事件物件的type屬性,如下所示。
class Btn extends React.Component { handle(e) { console.log(e.type); //"click" setTimeout(function() { console.log(e.type); //null }, 0); } render() { return <button onClick={this.handle}>提交</button>; } }
點選提交按鈕,在控制檯首先會輸出“click”,然後再輸出null。如果想要保持事件物件的屬性,那麼可以呼叫它的persist()方法。繼續上一個例子,只要在handle()方法中加一句e.persist(),如下程式碼所示,就能在定時器中輸出“click”。
handle(e) { console.log(e.type); //"click" setTimeout(function() { console.log(e.type); //"click" }, 0); e.persist(); }
四、this
事件處理程式中的this不會自動繫結,即預設是沒有指向的。在下面的例子中,點選提交按鈕,會先觸發註冊的handle()方法,然後在方法內輸出的this的值是undefined。
class Btn extends React.Component { handle() { console.log(this); //undefined } render() { return <button onClick={this.handle}>提交</button>; } }
有兩種方式可以解決this指向的問題,分別是箭頭函式和bind()方法。
1)箭頭函式
將事件處理程式改成箭頭函式的形式,就能糾正this的指向,如下程式碼所示。
class Btn extends React.Component { handle() { console.log(this); //Btn例項 } render() { return <button onClick={() => this.handle()}>提交</button>; } }
箭頭函式是不包含this的,在其內部使用的this是從上一層的作用域中繼承而來的,其指向的是函式宣告時所在的物件,即當前元件的例項,注意,像上面的<button>元素那樣,直接在其屬性中使用箭頭函式,會讓元件在每次渲染時,建立一個不同的回撥函式。
可以換一種寫法,避免每次新建函式,如下程式碼所示,將箭頭函式轉移到方法宣告的時候。
class Btn extends React.Component { handle = () => { console.log(this); //Btn例項 } render() { return <button onClick={this.handle}>提交</button>; } }
2)bind()
為事件處理程式呼叫bind()方法,同樣也能糾正this的指向,如下程式碼所示。
class Btn extends React.Component { handle() { console.log(this); //Btn例項 } render() { return <button onClick={this.handle.bind(this)}>提交</button>; } }
函式的bind()方法能建立一個繫結指定物件的函式,在上面的render()方法中,傳遞給bind()方法的this,其指向的正是當前元件的例項。除了在元素的屬性中呼叫bind()方法之外,還可以將其移到元件的建構函式中,如下所示。
class Btn extends React.Component { constructor(props) { super(props); this.handle = this.handle.bind(this); } handle() { console.log(this); //Btn例項 } render() { return <button onClick={this.handle}>提交</button>; } }
五、事件處理程式的引數
事件處理程式預設會接收一個事件物件,如果要向其傳遞額外的引數,那麼可以通過兩種方式實現。
第一種是用箭頭函式,如下程式碼所示,顯式地將所有引數傳遞給事件處理程式。
class Btn extends React.Component { handle(e, name) { console.log(e, name); } render() { return <button onClick={(e) => this.handle(e, "strick")}>提交</button>; } }
第二種是用bind()方法,如下程式碼所示,事件物件會被隱式的傳遞過去,並且必須位於事件處理程式引數列表的最後。
class Btn extends React.Component { handle(name, e) { console.log(name, e); } render() { return <button onClick={this.handle.bind(this, "strick")}>bind</button>; } }
&n