前提
最近通過閱讀React官方文檔的事件模塊,有了一些思考和收穫,在這裏記錄一下~
調用方法時需要手動綁定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也能達到我們的預期效果
合成事件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)
- 可以統一在組件掛載和卸載時做處理
- 只需要註冊一個事件即可,節省內存開銷
- 可以手動控制事件流程,特別是對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節點, 爲其註冊一個監聽事件並觸發來執行事件回調函數
- 最後,根據用戶的設置, 決定是否釋放事件。