本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出javascript
來個人 GitHub repo 閱讀完整的專題文章前端
來個人 我的博客 得到無與倫比的閱讀體驗java
用戶須要與UI產生交互,因此UI須要一個反應機制,用戶執行特定操做,就觸發特定的回調函數,開發者再把這個機制掛載到DOM元素上。react
DOM事件開發者再熟悉不過了,沒了它頁面就是死的。git
那麼React的事件機制有什麼特殊嗎?github
不誇張的說,React是一個UI虛擬機同樣的存在,在被掛載到頁面上以前,UI在React的全權掌控下。React會幹出什麼來誰也說不許。瀏覽器
讓咱們來看看React對DOM事件機制作了什麼手腳。babel
事件委託咱們都知道,由於有冒泡機制,開發者能夠在父級元素監聽事件,經過邏輯判斷使得只有子元素的事件纔會觸發監聽回調,這樣就實現了子元素的事件監聽委託給父元素。異步
在前端刀耕火種時期,事件委託解決了兩個痛點。函數
能夠看到,事件委託的紅利主要是性能提高,大量重複的事件監聽能夠交由一個事件監聽統一分發。
這樣的好處,React會不要?
不過,React作的更完全。
一個React應用只有一個事件監聽器,這個監聽器掛載在document
上。你沒聽錯,就是這麼粗暴。全部的事件都由這個監聽器統一分發。
組件掛載和更新時,會將綁定的事件分門別類的放進一個叫作EventPluginHub
的事件池裏。事件觸發時,根據事件產生的Event
對象找到觸發事件的組件,再經過組件標識和事件類型從事件池裏找到對應的事件監聽回調,而後就是打個響指。
原生DOM事件系統會爲每一個事件生成一個Event
對象,你去打印出來看看,這玩意有多少屬性。因此React一不作二不休,基於Event
對象建立了一個合成事件對象。它能解決什麼問題呢?
通常來講,當元素被卸載,元素綁定的事件監聽器也要清除。要否則JavaScript放個removeEventListener
接口出來幹什麼?
由於React實現了對事件的統一管理,因此這些髒活累活都自動幫你幹了,你不須要手動清除JSX上綁定的事件監聽器。這同時也能夠提升性能,由於開發者多半會忘記清除。固然原生事件React就無能爲力了。
說到原生事件,React合成事件與原生事件是什麼關係呢?
答案是沒有關係,互不干擾。
如下例子,即使阻止了冒泡,點擊按鈕依然會同時觸發document事件。放心,不是兼容性的問題。合成事件擁有獨立的冒泡機制,它只能阻止頂層的合成事件。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
componentDidMount() {
document.addEventListener('click', (event) => console.log(event));
}
handleClick = (event) => {
event.stopPropagation();
console.log(event);
}
}
export default App;
複製代碼
React知道你事多,因此在合成事件對象下面保存了原生事件對象nativeEvent
,以備不時之需。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
componentDidMount() {
document.addEventListener('click', (event) => console.log(event));
}
handleClick = (event) => {
event.nativeEvent.stopPropagation();
console.log(event);
}
}
export default App;
複製代碼
哈?你說仍是不行?
莫不是你計算機壞了,聽我一句勸,砸了吧。
別慌別慌,這裏還有一個知識點:
原生事件對象裏除了stopPropagation
以外還有stopImmediatePropagation
(是否是歷來沒用過),有什麼區別?
stopImmediatePropagation
不只會阻止頂層事件的冒泡,連自身元素綁定的其餘事件也會阻止。由於同一個元素能夠綁定多個事件,而事件觸發順序是根據綁定順序來的,只要使用了這個方法,它以後綁定的兄弟事件也別想蹦躂了。
那這跟React有什麼關係呢?
你忘了?上面講到,React有一套合成事件機制,全部事件都由document統一分發。
因此呀,別看這倆一個在button上,一個在document上,其實它們都是在document上觸發的。
這下理解了爲何要用stopImmediatePropagation
阻止冒泡了吧,它們是曹丕和曹植啊。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
componentDidMount() {
document.addEventListener('click', (event) => console.log(event));
}
handleClick = (event) => {
event.nativeEvent.stopImmediatePropagation();
console.log(event);
}
}
export default App;
複製代碼
不信再看一個衍生例子。
這回stopImmediatePropagation
不只不能阻止body事件,body事件還會先於button觸發。鐵證,React全部事件都是由document統一分發的。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
componentDidMount() {
document.body.addEventListener('click', (event) => console.log(event));
}
handleClick = (event) => {
event.nativeEvent.stopImmediatePropagation();
console.log(event);
}
}
export default App;
複製代碼
先來看例子,你們以爲最終state裏的value是什麼?
答案是程序崩潰。
別看了,沒有語法錯誤。
報錯信息裏說Cannot read property 'value' of null
,說明target爲空,問題是target怎麼會爲空呢?
癥結就在於React追求極致的性能。在合成事件機制裏,一旦事件監聽回調被執行,合成事件對象就會被銷燬,而setState的回調是異步的,等它執行的時候合成事件對象早就被銷燬了。這就是target爲空的緣由。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
handleClick = (event) => {
this.setState((prevState) => ({ value: event.target.value }));
}
}
export default App;
複製代碼
若是實在有這樣的需求,React也有錦囊妙計:event.persist()
。
這就是告訴React,你別回收了,我還要拿去釣妹子呢。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
handleClick = (event) => {
event.persist();
this.setState((prevState) => ({ value: event.target.value }));
}
}
export default App;
複製代碼
咱們再來看一種狀況。
臥槽,怎麼又能夠了?我啥也沒跟React說呀。
咱們都說setState是異步(或者說批量更新)的,那是說渲染異步,而賦值給value是同步的。
因此這個時候value是有值的。
那爲何回調形式的setState得不到值呢?回調嘛,你想嘛,是同步仍是異步。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
handleClick = (event) => {
this.setState({ value: event.target.value });
}
}
export default App;
複製代碼
鬼知道JavaScript裏的this幹倒了多少人。
其實,要弄清楚this的指向,只要找到調用者就好了。調用者,就是this的題眼。
爲何例子中的函數在非嚴格模式下指向window,而在嚴格模式下指向undefined呢?
由於在JavaScript刀耕火種時代,window既是窗口對象,也是全局對象。而全部的全局變量(包括函數)都掛載在window下面。
非嚴格模式下這個函數的調用者就是window。
後面人們以爲這樣太八路軍了,甚至有人以爲這是JavaScript最大的設計失誤。因此以後的嚴格模式、class類和ESModule的全局變量都再也不掛載到window上,反正能找補一點是一點。
因此嚴格模式下這個函數沒有調用者,或者叫神之調用,因此this指向undefined。
function something() {
console.log(this);
}
複製代碼
科普了一下this,咱們來看看this在React中有什麼幺蛾子。
(僞裝)咱們都知道,下面例子的this打印出來是undefined。
關鍵是爲何?
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
handleClick() {
console.log(this);
}
}
export default App;
複製代碼
先看一個別的例子。
obj.something是一個函數,action也是一個函數,區別在於調用者。一個函數一旦被從新賦值,它的調用者就可能發生變化。
const obj = {
something: function() {
console.log(this);
},
};
obj.something(); // 打印obj
const action = obj.something;
action(); // (假設嚴格模式)打印undefined
複製代碼
再回到以前的例子,關鍵在這一句onClick={this.handleClick}
,注意回調已經被從新賦值了,無論未來它的調用者是誰,這時它已經和組件實例無關了。
而後,咱們要知道,React會把同一類型的事件push到一個隊列裏,一旦document監聽到這類事件,就會依次執行隊列裏的回調,直到冒泡被阻止。
就像這樣[handleDivClick, handleButtonClick]
,想象一下被這樣處理以後,執行時調用者是誰。
這就是上面例子打印出來是undefined的緣由。
其實React早期版本,程序會自動綁定this到組件實例,可是有人以爲這樣會使部分開發者覺得this指向組件實例就是理所應當的,因此取消了這一操做。
因而呢,開發者要手動綁定this。咱們來看看綁定this的花樣。
簡單粗暴對吧。再怎麼狸貓換太子,我都綁的死死的。
不過呢,bind的性能是堪憂的。並且你發現沒有,每一次從新render都會從新bind一次。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick.bind(this)}>Click</button>
);
}
handleClick() {
console.log(this);
}
}
export default App;
複製代碼
箭頭函數會繼承父做用域的this,這裏的父做用域固然就是組件實例。
但是得額外包裹一層箭頭函數,並且每次觸發事件都會生成一個箭頭函數。
固然事件須要傳參的時候沒的說,必須得包裹一層箭頭函數。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={() => this.handleClick()}>Click</button>
);
}
handleClick() {
console.log(this);
}
}
export default App;
複製代碼
這也是React官方推薦的寫法。
此寫法的意思是:把一個綁定了this的回調賦值給實例的屬性。
缺點是若是事件比較多,構造函數裏會有點擁擠。
並且往深層處想,這個回調被掛載在了原型上,同時也被掛載在了實例上。重複掛載。
import React, { Component } from 'react';
class App extends Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
handleClick() {
console.log(this);
}
}
export default App;
複製代碼
這種寫法叫作屬性初始化器(Property Initializers)。目前還不是JavaScript正式的語法,不過babel能夠提早讓開發者使用。
首先說明,該寫法的關鍵不是直接寫在實例上,而是箭頭函數。由於箭頭函數會繼承父做用域的this,因此回調中的this指向組件實例。
不信你把箭頭函數改爲匿名函數試試。
那我能不能把箭頭函數寫在原型上呢?你甭管我用什麼辦法。
也是能夠的,只是有點麻煩。
屬性初始化器的寫法不會將回調重複掛載,不須要重複綁定,語法也至關優雅。
等成爲了JavaScript正式的語法,React官方必定會推薦這種寫法的。
import React, { Component } from 'react';
class App extends Component {
render() {
return (
<button onClick={this.handleClick}>Click</button>
);
}
handleClick = () => {
console.log(this);
}
}
export default App;
複製代碼
React專題一覽