前提
最近經過閱讀React官方文檔的事件模塊,發現了其主要提到了如下三個點javascript
- 調用方法時須要手動綁定this
- React事件是一種合成事件SyntheticEvent,什麼是合成事件?
- 事件屬性會在事件調用後被回收,即不能異步訪問
- 事件機制的源碼分析
1).註冊階段源碼分析
2).觸發階段源碼分析
3).總結相關流程
帶着問題,經過查詢資料和源碼來探尋~html
1.調用方法時須要手動綁定this
先從一段官方代碼看起:java
代碼中的註釋提到了一句話:react
This binding is necessary to make `this` work in the callback瀏覽器
this的綁定是必須的,其實這一塊是比較容易理解的, 由於這並非React的一個特殊點, 而是Javascript這門語言的特性。babel
能夠看到,調用的是this.handleClick函數,handleClick函數裏面又讀取到了this屬性,可是該函數的調用位置又是在render函數裏面,render返回的是一個JSX,最後通過babel編譯成調用React.createElement函數,異步
在這以前,咱們掌握的是this永遠指向的是最後調用它的對象,通過這樣的一個轉換, 實際上this最後指向的是undeined了, 那麼調用handleClick函數天然會報錯。async
固然,若是你不在函數裏面使用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節點, 爲其註冊一個監聽事件並觸發來執行事件回調函數
- 最後,根據用戶的設置, 決定是否釋放事件。