React事件雜記及原始碼分析
前提
最近通過閱讀React官方文件的事件模組,發現了其主要提到了以下三個點
- 呼叫方法時需要手動繫結this
- React事件是一種合成事件 ofollow,noindex" target="_blank">SyntheticEvent ,什麼是合成事件?
- 事件屬性會在事件呼叫後被回收,即不能非同步訪問
-
事件機制的原始碼分析
1).
註冊階段原始碼分析
2).觸發階段原始碼分析
3).總結相關流程
帶著問題,通過查詢資料和原始碼來探尋~
1.呼叫方法時需要手動繫結 this
先從一段官方程式碼看起:
程式碼中的註釋提到了一句話:
This binding is necessary to make `this` work in the callback
this的繫結是必須的,其實這一塊是比較容易理解的, 因為 這並不是 React 的一個特殊點, 而是 Javascript 這門語言的特性 。
可以看到,呼叫的是 this.handleClick 函式, handleClick 函式裡面又讀取到了 this 屬性,但是該函式的呼叫位置又是在 render 函式裡面, render 返回的是一個 JSX ,最後經過 babel 編譯成呼叫 React.createElement 函式,
在這之前,我們掌握的是 this永遠指向的是最後呼叫它的物件 ,經過這樣的一個轉換, 實際上 this 最後指向的是 undeined 了, 那麼呼叫 handleClick 函式自然會報錯。
當然,如果你不在函式裡面使用this的話,通常會沒事,但並不建議這麼做。
關於this的指向與function的原理,推薦閱讀 how functions work in JavaScript
既然知道了是因為 this 的指向原因而採用繫結的做法,那當然可以用箭頭函式來解決了, 箭頭函式中的this是在定義函式的時候繫結 ,也就是說 this是繼承自父執行上下文,如下:
這樣this也能達到我們的預期效果
React事件是一種合成事件 SyntheticEvent ,什麼是合成事件?
先從官方上的一段話看起,他的意思是合成事件是React根據 W3C 標準定義的,無需擔心瀏覽器之間的差異
Here, e is a synthetic event. React defines these synthetic events according to the W3C spec , so you don’t need to worry about cross-browser compatibility
這樣看起來 React 的合成事件只是相容瀏覽器? 答案當然是遠遠不止啦!
在探尋其優點之前,我們先看一下其是怎樣的一個機制。
React的事件機制其實網上有很多同學都分析過了, 他並沒有將事件註冊在對應的元素或者元件上面,而是 通過委託的方式,將所有的事件都註冊到了 document 物件上,並統一呼叫一個 dispatch 回撥函式 ,其流程圖如下
我們也可以從一個實際的簡單例子看看:
我們把回撥函式繫結到了 button 上,但是在事件上卻沒有看到 button 元素, 但是卻有 document ,並且可以看到他的回撥函式就是 dispatchInteractiveEvent
最後觸發事件的回撥函式時,在原生的 DOM 會傳入一個事件屬性 event ,但是因為 React 將 所有事件委託給 document 處理, 那麼這個 event 就和我們想要的不一樣,如 target 指向的是 document ,於是 React 就有了自己的一個合成事件,通過一個叫 SyntheticEvent 的基類來生成所需要的事件屬性,並傳入回撥函式作為方法。
說到底, React就是把所有事件委託給document處理 , 那麼這樣做有什麼好處:
- 可以統一在元件掛載和解除安裝時做處理
- 只需要註冊一個事件即可,節省記憶體開銷
- 可以手動控制事件流程,特別是對state的batch處理(參考React系列的setState)
事件屬性會在事件呼叫後被回收,即不能非同步訪問
老規矩,先上一段程式碼:
可以看到在 setTimeout 函式中,訪問事件屬性是 null 。這是為啥 ?
其實這也是合成事件的一個優化手段。 React會在事件呼叫完成後清理掉屬性,否則每點選一次就生成一個事件,那麼記憶體的開銷會越來越大 ,具體的程式碼可以在後面的原始碼分析中看到:
當然了, React 也可以手動設定不回收,如下:
If you want to access the event properties in an asynchronous way, you should call event.persist() on the event
我們可以通過呼叫 event,persist 來設定不回收。
事件機制的原始碼分析
註冊階段
首先在某一個任務單元fiber呼叫 compeleteWork 函式時, React 會判斷其是否具有事件屬性, 如果有則呼叫 ensureListeningTo 函式
ensureListeningTo函式主要是獲取到 document 物件, 並呼叫 listenTo 函式
listerTo函式 主要是通過呼叫 trapBubbledEvent 或者 trapCapturedEvent 將事件放在 document 事件上監聽
trapBubbledEvent主要是監聽事件, 但也可以看出, 所有事件最後觸發的都是註冊在 document 上的 dispatch 函式
呼叫階段
dispatch函式, 主要是獲取實際觸發的元素以及對應的 fiber , 最後呼叫 batchedUpdates 函式, batchedUpdates 函式裡面的邏輯主要是關於 setState 的,這裡主要是看事件機制, 只要知道最後 呼叫的是handleTopLevel(bookkeeping)就好
handleTopLevel 函式主要是拿到需要觸發事件的相關 fiber , 並呼叫 runExtractedEventsInBatch 函式
extractEvents函式是一個生成 React 事件的函式, React 事件是通過繼承一個通用類 SyntheticEvent 生成的,如 一個滑鼠事件的生成
React事件內部做了優化, 只要生成過 SyntheticMouseEvent 類, 就會再 釋放事件的時候將這個類儲存起來,在下一個事件觸發時可以直接使用
React生成事件後, 會呼叫 accumulateTwoPhaseDispatches(event) 函式,該函式一直追溯下去, 最後會呼叫 traverseTwoPhase 函式,
traverseTwoPhase函式主要是獲取祖先元件的 fiber, 並進行捕獲和冒泡的階段處理
accumulateDirectionalDispatches函式相對簡單, 就是把 fiber 上對應的事件函式賦值給 evnet 的 _dispatchListeners 屬性
React事件獲取完成後, 回到 runExtractedEventsInBatch 函式繼續呼叫 runEventsInBatch(events, false); 函式的中間作了一系列的處理, 但最後執行的是 executeDispatchesAndRelease 函式
executeDispatchesAndRelease函式會在 執行完事件後判斷使用者是否有設定不銷燬事件, 如果沒有, 則銷燬事件並儲存事件類, 一個事件類例項一次並重復使用, 這也是為什麼官方提到事件屬性只能在當前迴圈中讀到
繼續往下走, 最後執行的函式是 invokeGuardedCallbackDev , 該函式通過註冊一個自定義的元素 <react> 和自定義的事件, 並觸發它來達到執行回撥函式的功能
最後, 總結下相關的流程:
- 通過 Fiber 中的屬性, 將事件統一委託 註冊到 document 上,併為 document 註冊相應的事件回撥函式 dispatch 函式。
- 先獲取實際觸發元素對應的 fiber.
- 生成相應的 React 事件屬性 event ,將對應的回撥函式賦值給event._dispatchListeners, 將fiber賦值給event._dispatchInstances
- 通過 fiber 向上遍歷, 找到所有的祖先 fiber , 並按原生事件的機制先捕獲後冒泡的執行事件
- 註冊一個 react 節點, 為其註冊一個監聽事件並觸發來執行事件回撥函式
- 最後,根據使用者的設定, 決定是否釋放事件。