React 是一個 Facebook 開源的,用於構建用戶界面的 JavaScript 庫。
React 目的在於解決:構建隨着時間數據不斷變化的大規模應用程序。
其中 React 合成事件是較爲重要的知識點,閱讀完本文,你將收穫:javascript
接下來和我一塊兒開始學習吧~html
React 合成事件(SyntheticEvent)是 React 模擬原生 DOM 事件全部能力的一個事件對象,即瀏覽器原生事件的跨瀏覽器包裝器。它根據 W3C 規範 來定義合成事件,兼容全部瀏覽器,擁有與瀏覽器原生事件相同的接口。
看個簡單示例:java
const button = <button onClick={handleClick}>Leo 按鈕</button>
在 React 中,全部事件都是合成的,不是原生 DOM 事件,但能夠經過 e.nativeEvent
屬性獲取 DOM 事件。react
const handleClick = (e) => console.log(e.nativeEvent);; const button = <button onClick={handleClick}>Leo 按鈕</button>
學習一個新知識的時候,必定要知道爲何會出現這個技術。
那麼 React 爲何使用合成事件?其主要有三個目的:git
React 採用的是頂層事件代理機制,可以保證冒泡一致性,能夠跨瀏覽器執行。React 提供的合成事件用來抹平不一樣瀏覽器事件對象之間的差別,將不一樣平臺事件模擬合成事件。github
事件對象可能會被頻繁建立和回收,所以 React 引入事件池,在事件池中獲取或釋放事件對象。即 React 事件對象不會被釋放掉,而是存放進一個數組中,當事件觸發,就從這個數組中彈出,避免頻繁地去建立和銷燬(垃圾回收)。typescript
本文不介紹源碼啦,對具體實現的源碼有興趣的朋友能夠查閱: 《React SyntheticEvent》 。
在開始介紹 React 合成事件以前,咱們先簡單回顧 JavaScript 原生事件中幾個重要知識點:數組
當某個元素觸發某個事件(如 onclick
),頂層對象 document
就會發出一個事件流,隨着 DOM 樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程當中,事件相應的監聽函數是不會被觸發的。瀏覽器
當到達目標元素以後,執行目標元素該事件相應的處理函數。若是沒有綁定監聽函數,那就不執行。性能優化
從目標元素開始,往頂層元素傳播。途中若是有節點綁定了相應的事件處理函數,這些函數都會被觸發一次。若是想阻止事件起泡,可使用 e.stopPropagation()
或者 e.cancelBubble=true
(IE)來阻止事件的冒泡傳播。
簡單理解就是將一個響應事件委託到另外一個元素。
當子節點被點擊時,click
事件向上冒泡,父節點捕獲到事件後,咱們判斷是否爲所需的節點,而後進行處理。其優勢在於減小內存消耗和動態綁定事件。
React 事件與原生事件很類似,但不徹底相同。這裏列舉幾個常見區別:
原生事件命名爲純小寫(onclick, onblur),而 React 事件命名採用小駝峯式(camelCase),如 onClick
等:
// 原生事件綁定方式 <button onclick="handleClick()">Leo 按鈕命名</button> // React 合成事件綁定方式 const button = <button onClick={handleClick}>Leo 按鈕命名</button>
原生事件中事件處理函數爲字符串,在 React JSX 語法中,傳入一個函數做爲事件處理函數。
// 原生事件 事件處理函數寫法 <button onclick="handleClick()">Leo 按鈕命名</button> // React 合成事件 事件處理函數寫法 const button = <button onClick={handleClick}>Leo 按鈕命名</button>
在原生事件中,能夠經過返回 false
方式來阻止默認行爲,可是在 React 中,須要顯式使用 preventDefault()
方法來阻止。
這裏以阻止 <a>
標籤默認打開新頁面爲例,介紹兩種事件區別:
// 原生事件阻止默認行爲方式 <a href="https://www.pingan8787.com" onclick="console.log('Leo 阻止原生事件~'); return false" > Leo 阻止原生事件 </a> // React 事件阻止默認行爲方式 const handleClick = e => { e.preventDefault(); console.log('Leo 阻止原生事件~'); } const clickElement = <a href="https://www.pingan8787.com" onClick={handleClick}> Leo 阻止原生事件 </a>
小結前面幾點區別:
原生事件 | React 事件 | |
---|---|---|
事件名稱命名方式 | 名稱所有小寫<br/>(onclick, onblur) | 名稱採用小駝峯<br/>(onClick, onBlur) |
事件處理函數語法 | 字符串 | 函數 |
阻止默認行爲方式 | 事件返回 false |
使用 e.preventDefault() 方法 |
在 React 中,「合成事件」會以事件委託(Event Delegation)方式綁定在組件最上層,並在組件卸載(unmount)階段自動銷燬綁定的事件。這裏咱們手寫一個簡單示例來觀察 React 事件和原生事件的執行順序:
class App extends React.Component<any, any> { parentRef: any; childRef: any; constructor(props: any) { super(props); this.parentRef = React.createRef(); this.childRef = React.createRef(); } componentDidMount() { console.log("React componentDidMount!"); this.parentRef.current?.addEventListener("click", () => { console.log("原生事件:父元素 DOM 事件監聽!"); }); this.childRef.current?.addEventListener("click", () => { console.log("原生事件:子元素 DOM 事件監聽!"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件監聽!"); }); } parentClickFun = () => { console.log("React 事件:父元素事件監聽!"); }; childClickFun = () => { console.log("React 事件:子元素事件監聽!"); }; render() { return ( <div ref={this.parentRef} onClick={this.parentClickFun}> <div ref={this.childRef} onClick={this.childClickFun}> 分析事件執行順序 </div> </div> ); } } export default App;
觸發事件後,能夠看到控制檯輸出:
原生事件:子元素 DOM 事件監聽! 原生事件:父元素 DOM 事件監聽! React 事件:子元素事件監聽! React 事件:父元素事件監聽! 原生事件:document DOM 事件監聽!
經過上面流程,咱們能夠理解:
document
對象上;document
對象後,再處理 React 事件;document
上掛載的事件。合成事件對象池,是 React 事件系統提供的一種性能優化方式。合成事件對象在事件池統一管理,不一樣類型的合成事件具備不一樣的事件池。
關於「事件池是如何工做」的問題,能夠看看下面圖片:
(圖片來自:ReactDeveloper https://juejin.cn/post/6844903862285893639)
React 事件池僅支持在 React 16 及更早版本中,在 React 17 已經不使用事件池。
下面以 React 16 版本爲例:
function handleChange(e) { console.log("原始數據:", e.target) setTimeout(() => { console.log("定時任務 e.target:", e.target); // null console.log("定時任務:e:", e); }, 100); } function App() { return ( <div className="App"> <button onClick={handleChange}>測試事件池</button> </div> ); } export default App;
能夠看到輸出:
在 React 16 及以前的版本,合成事件對象的事件處理函數所有被調用以後,全部屬性都會被置爲 null
。這時,若是咱們須要在事件處理函數運行以後獲取事件對象的屬性,可使用 React 提供的 e.persist()
方法,保留全部屬性:
// 只修改 handleChange 方法,其餘不變 function handleChange(e) { // 只增長 persist() 執行 e.persist(); console.log("原始數據:", e.target) setTimeout(() => { console.log("定時任務 e.target:", e.target); // null console.log("定時任務:e:", e); }, 100); }
再看下結果:
因爲 Web 端的 React 17 不使用事件池,全部不會存在上述「全部屬性都會被置爲 null
」的問題。
在 React 中,JSX 回調函數中的 this 常常會出問題,在 Class 中方法不會默認綁定 this,就會出現下面狀況, this.funName
值爲 undefined
:
class App extends React.Component<any, any> { childClickFun = () => { console.log("React 事件"); }; clickFun() { console.log("React this 指向問題", this.childClickFun); // undefined } render() { return ( <div onClick={this.clickFun}>React this 指向問題</div> ); } } export default App;
咱們有 2 種方式解決這個問題:
bind
方法綁定 this
:class App extends React.Component<any, any> { constructor(props: any) { super(props); this.clickFun = this.clickFun.bind(this); } // 省略其餘代碼 } export default App;
this
的方法改寫爲使用箭頭函數定義:class App extends React.Component<any, any> { clickFun = () => { console.log("React this 指向問題", this.childClickFun); // undefined } // 省略其餘代碼 } export default App;
或者在回調函數中使用箭頭函數:
class App extends React.Component<any, any> { // 省略其餘代碼 clickFun() { console.log("React this 指向問題", this.childClickFun); // undefined } render() { return ( <div onClick={() => this.clickFun()}>React this 指向問題</div> ); } } export default App;
常常在遍歷列表時,須要向事件傳遞額外參數,如 id
等,來指定須要操做的數據,在 React 中,可使用 2 種方式向事件傳參:
const List = [1,2,3,4]; class App extends React.Component<any, any> { // 省略其餘代碼 clickFun (id) {console.log('當前點擊:', id)} render() { return ( <div> <h1>第一種:經過 bind 綁定 this 傳參</h1> { List.map(item => <div onClick={this.clickFun.bind(this, item)}>按鈕:{item}</div>) } <h1>第二種:經過箭頭函數綁定 this 傳參</h1> { List.map(item => <div onClick={() => this.clickFun(item)}>按鈕:{item}</div>) } </div> ); } } export default App;
這兩種方式是等價的:
Function.prototype.bind
實現;官網文檔描述了:
從 v0.14 開始,事件處理器返回 false 時,再也不阻止事件傳遞。你能夠酌情手動調用 e.stopPropagation() 或 e.preventDefault() 做爲替代方案。
也就是說,在 React 合成事件中,須要阻止冒泡時,可使用 e.stopPropagation()
或 e.preventDefault()
方法來解決,另外還可使用 e.nativeEvent.stopImmediatePropagation()
方法解決。
對於開發者來講,更但願使用 e.stopPropagation()
方法來阻止當前 DOM 事件冒泡,但事實上,從前兩節介紹的執行順序可知,e.stopPropagation()
只能阻止合成事件間冒泡,即下層的合成事件,不會冒泡到上層的合成事件。事件自己還都是在 document 上執行。因此最多隻能阻止 document 事件不能再冒泡到 window 上。
class App extends React.Component<any, any> { parentRef: any; childRef: any; constructor(props: any) { super(props); this.parentRef = React.createRef(); } componentDidMount() { this.parentRef.current?.addEventListener("click", () => { console.log("阻止原生事件冒泡~"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件監聽!"); }); } parentClickFun = (e: any) => { e.stopPropagation(); console.log("阻止合成事件冒泡~"); }; render() { return ( <div ref={this.parentRef} onClick={this.parentClickFun}> 點擊測試「合成事件和原生事件是否能夠混用」 </div> ); } } export default App;
輸出結果:
阻止原生事件冒泡~ 阻止合成事件冒泡~
該方法能夠阻止監聽同一事件的其餘事件監聽器被調用。
在 React 中,一個組件只能綁定一個同類型的事件監聽器,當重複定義時,後面的監聽器會覆蓋以前的。
事實上 nativeEvent 的 stopImmediatePropagation
只能阻止綁定在 document 上的事件監聽器。而合成事件上的 e.nativeEvent.stopImmediatePropagation()
能阻止合成事件不會冒泡到 document 上。
舉一個實際案例:實現點擊空白處關閉菜單的功能:
當菜單打開時,在 document 上動態註冊事件,用來關閉菜單。
在菜單關閉的一刻,在 document 上移除該事件,這樣就不會重複執行該事件,浪費性能,也能夠在 window 上註冊事件,這樣能夠避開 document。
**
合成事件和原生事件最好不要混用。
原生事件中若是執行了stopPropagation
方法,則會致使其餘React
事件失效。由於全部元素的事件將沒法冒泡到document
上。
經過前面介紹的二者事件執行順序來看,全部的 React 事件都將沒法被註冊。經過代碼一塊兒看看:
class App extends React.Component<any, any> { parentRef: any; childRef: any; constructor(props: any) { super(props); this.parentRef = React.createRef(); } componentDidMount() { this.parentRef.current?.addEventListener("click", (e: any) => { e.stopPropagation(); console.log("阻止原生事件冒泡~"); }); document.addEventListener("click", (e) => { console.log("原生事件:document DOM 事件監聽!"); }); } parentClickFun = (e: any) => { console.log("阻止合成事件冒泡~"); }; render() { return ( <div ref={this.parentRef} onClick={this.parentClickFun}> 點擊測試「合成事件和原生事件是否能夠混用」 </div> ); } } export default App;
輸出結果:
阻止原生事件冒泡~
好了,本文就寫到這裏,建議你們能夠再回去看下官方文檔《合成事件》《事件處理》章節理解,有興趣的朋友也能夠閱讀源碼《React SyntheticEvent.js》。
最後在回顧下本文學習目標:
你是否都清楚了?歡迎一塊兒討論學習。
1.《事件處理與合成事件(react)》
2.官方文檔《合成事件》《事件處理》
3.《React合成事件和DOM原生事件混用須知》
4.《React 合成事件系統之事件池》