React-事件機制雜記

前提

最近通過閱讀React官方文檔的事件模塊,有了一些思考和收穫,在這裏記錄一下~

調用方法時需要手動綁定this

先從一段官方代碼看起:
image

代碼中的註釋提到了一句話:

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也能達到我們的預期效果
image

合成事件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回調函數,其流程圖如下

image

我們也可以從一個實際的簡單例子看看:

我們把回調函數綁定到了button上,但是在事件上卻沒有看到button元素, 但是卻有document,並且可以看到他的回調函數就是dispatchInteractiveEvent

image
image

最後觸發事件的回調函數時,在原生的DOM會傳入一個事件屬性event,但是因爲React將 所有事件委託給document處理, 那麼這個event就和我們想要的不一樣,如target指向的是document,於是React就有了自己的一個合成事件,通過一個叫SyntheticEvent的基類來生成所需要的事件屬性,並傳入回調函數作爲方法。

說到底,React就是把所有事件委託給document處理, 那麼這樣做有什麼好處:

可以統一在組件掛載和卸載時做處理
只需要註冊一個事件即可,節省內存開銷
可以手動控制事件流程,特別是對state的batch處理(參考React系列的setState

  • 可以統一在組件掛載和卸載時做處理
  • 只需要註冊一個事件即可,節省內存開銷
  • 可以手動控制事件流程,特別是對state的batch處理(參考React系列的setState

事件屬性會在事件調用後被回收,即不能異步訪問

老規矩,先上一段代碼:
image
可以看到在setTimeout函數中,訪問事件屬性是null。這是爲啥?

其實這也是合成事件的一個優化手段。 React會在事件調用完成後清理掉屬性,否則每點擊一次就生成一個事件,那麼內存的開銷會越來越大,具體的代碼可以在後面的源碼分析中看到:

image

當然了, 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函數
image

listerTo函數 主要是通過調用trapBubbledEvent或者trapCapturedEvent將事件放在document事件上監聽
image

trapBubbledEvent主要是監聽事件, 但也可以看出, 所有事件最後觸發的都是註冊在document上的dispatch函數
image

調用階段

dispatch函數, 主要是獲取實際觸發的元素以及對應的fiber, 最後調用batchedUpdates函數, batchedUpdates函數裏面的邏輯主要是關於setState的,這裏主要是看事件機制, 只要知道最後調用的是handleTopLevel(bookkeeping)就好

image

handleTopLevel函數主要是拿到需要觸發事件的相關fiber, 並調用runExtractedEventsInBatch函數
image

extractEvents函數是一個生成React事件的函數,React事件是通過繼承一個通用類SyntheticEvent生成的,如一個鼠標事件的生成

image

React事件內部做了優化, 只要生成過SyntheticMouseEvent類, 就會再釋放事件的時候將這個類存儲起來,在下一個事件觸發時可以直接使用

image

React生成事件後, 會調用accumulateTwoPhaseDispatches(event)函數,該函數一直追溯下去, 最後會調用traverseTwoPhase函數,

traverseTwoPhase函數主要是獲取祖先組件的fiber, 並進行捕獲和冒泡的階段處理

image

accumulateDirectionalDispatches函數相對簡單, 就是把fiber上對應的事件函數賦值給evnet的_dispatchListeners屬性

image

React事件獲取完成後, 回到runExtractedEventsInBatch函數繼續調用runEventsInBatch(events, false); 函數的中間作了一系列的處理, 但最後執行的是executeDispatchesAndRelease函數

executeDispatchesAndRelease函數會在執行完事件後判斷用戶是否有設置不銷燬事件, 如果沒有, 則銷燬事件並保存事件類, 一個事件類實例一次並重複使用, 這也是爲什麼官方提到事件屬性只能在當前循環中讀到

image

繼續往下走, 最後執行的函數是invokeGuardedCallbackDev, 該函數通過註冊一個自定義的元素<react>和自定義的事件, 並觸發它來達到執行回調函數的功能

image

流程總結

  1. 通過Fiber中的屬性, 將事件統一委託 註冊到document上,併爲document註冊相應的事件回調函數 dispatch函數。
  2. 先獲取實際觸發元素對應的fiber.
  3. 生成相應的React事件屬性event,將對應的回調函數賦值給event._dispatchListeners, 將fiber賦值給event._dispatchInstances
  4. 通過fiber向上遍歷, 找到所有的祖先fiber, 並按原生事件的機制先捕獲後冒泡的執行事件
  5. 註冊一個react節點, 爲其註冊一個監聽事件並觸發來執行事件回調函數
  6. 最後,根據用戶的設置, 決定是否釋放事件。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章