前面的文章介紹了 React 的 JSX 語法、元件的建立方式、元件的屬性、元件的內部狀態以及元件的生命週期。另外,還順帶說了各個知識點要重點注意的事情,以及我在專案實踐中的一些經驗。如果你覺得對自己有幫助,可以通過 玩轉 React(一)- 前言 中的文章目錄進行閱讀。
另外,為了方便大家更好地交流 React、分享前端開發經驗,我建了一個微信群,由於微信群二維碼有時間限制,你可以先加我好友(我的微信:leobaba88),驗證資訊 玩轉 React
,我會拉你入群,歡迎大家,下面是我的微信二維碼。
好的,言歸正傳,今天我們說一下在 React 中是如何處理事件的。事件處理是前端開發過程中非常重要的一部分,通過事件處理機制,我們的前端應用可以響應使用者的各種操作,從而實現一個富互動的前端應用。
內容摘要
- 如何為 React 的內建元件設定事件處理函式。
- React 事件物件與瀏覽器原生 DOM 事件物件的區別。
- 預設情況下不能以非同步的方式使用事件物件,如在
setTimeout
中。 - 不要在元件中使用
addEventListener
註冊事件處理函式,有坑。 - 繫結事件處理函式 this 指向的四中方式以及他們的優缺點。
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(preState => ({ count: preState.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"));複製程式碼
如何繫結事件處理函式的 this
在以類繼承的方式定義的元件中,為了能方便地呼叫當前元件的其他成員方法或屬性(如:this.state),通常需要將事件處理函式執行時的 this 指向當前元件例項。
如下面的示例:
class Link extends React.Component {
constructor(props) {
super(props);
this.state = {
count: 0
}
}
handleClick(e) {
e.preventDefault();
this.setState(preState => ({ count: preState.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(preState => ({ count: preState.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(preState => ({ count: preState.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 規範中。參考:github.com/tc39/propos…
不過這也沒太大關係。
總結
本文的內容並不多,可能說的有點囉嗦。簡單總結一下,React 中通過設定元件的 事件屬性 來註冊事件,React 內部自己實現了一套包含冒泡、捕獲邏輯在內的事件機制,所以儘量不要使用 addEventListener
,除非你知道自己在幹什麼。有四種為事件處理函式繫結 this 的方法,推薦使用類屬性定義的方式來定義處理函式,如果你不太在意哪一點效能開銷的話,可以使用箭頭函式包裝真實事件回撥的方式。另外,事件物件在 React 中是被複用的,事件回撥被執行以後,事件物件的所有屬性會被重置為 null,所以不要在非同步的過程中使用事件物件。
好了,有什麼疑問可以加微信群交流,我的微訊號:leobaba88
,驗證資訊:玩轉 React。
PS:本系列的所有文章將在 segmentfault 和 掘金 同步釋出。
本作品保留所有權利。未獲得許可人許可前,不允許他人複製、發行、展覽和表演作品。不允許他人基於該作品創作演繹作品 。