React16事件機制完全解讀

react.js事件機制node

寫這篇文章的原因:

一:在給input綁定事件的時候,很好奇爲什麼onChange的交互形式居然和onInput如出一轍。 由於原生的change事件是在input失去焦點的時候觸發,但react的onChange則徹底不一樣。
二:阻止事件冒泡,event.preventDeault()沒辦法阻止原生事件冒泡。

從react事件的三個階段講起,會略微說起下react15的事件機制。

一 事件註冊
二 事件合成
三 事件派發

react事件註冊

1. 一切從createInstance,建立dom實例開始講起。react

  • react16引入了fiber的概念,講fiber的文章不少,這裏就很少闡述。能夠簡單的先理解爲fiber Tree 略微等於 vDOM Tree(固然實際確定有差異)。

當react遍歷tree建立真實dom實例的時候作了什麼???數組

clipboard.png

重點就在這幾行代碼。domElement等於真實建立的dom,裏面調用的就是咱們熟悉的createElement。而precacheFiberNode和updateFiberProps兩個方法分別給domElement(真實dom)添加了兩個屬性。全部react16項目中的dom都會擁有這兩個屬性,而且這個兩個屬性的屬性名在同一個項目中是一致的。瀏覽器

clipboard.png 圖片描述dom

precacheFiberNode方法中 設置 node[internalInstanceKey] = 一個new FiberNode()的實例函數

clipboard.png

updateFiberProps方法中 設置node[internalEventHandlersKey] = props 。這裏的props就是<Component className='xxx' onClick={} /> 這裏的屬性。this

clipboard.png

劃重點了! 這兩個方法等於將真實dom和fiber,props直接關聯到了一塊兒,相互引用,這點很重要,後續會用到,至關於前期準備。 spa

2. listenTo
createInstance後(仍然在fiber tree遍歷中),程序兜兜轉轉,層層調用,最後終於走到了關鍵方法listenTo這裏來,全部的事件註冊邏輯都在這裏實現。react作了一系列的兼容處理,儘量的保證各個瀏覽器端交互一致。code

clipboard.png

ensureListeningTo裏面調用的就listenTo。首先去遍歷props中的屬性。而registrationNameModules.hasOwnProperty(propKey),registrationNameModules是一個事件名的集合,幾乎包含了全部的常見事件,這也就是若是你寫一些稀奇古怪的事件,react是不識別的。若是斷定props中的屬性 如onClick在registrationNameModules中,而且值typeof === function,則會進入到listenTo中。對象

clipboard.png

回到listenTo這個方法,他接收兩個參數,一個是事件名如onClick,一個是contentDocumentHandle,一般就是document。
重點講下這個 var dependencies = registrationNameDependencies[registrationName]; registrationNameDependencies這個東西理解爲事件依賴。 就是說註冊某個事件,react會強制依賴其餘事件。而具體是哪些依賴,react的event模塊已經幫咱們處理了,就不深層次探討。
舉例onChange事件就依賴了下面的一些事件
registrationNameDependencies = {

onChange = ['topBlur', 'topChange', 'topClick', 'topInput', 'topKey' ....還有]

}
這裏react的事件都加上了top前綴,沒什麼太大的深層次含義,可能就是爲了區分下吧,畢竟後續用到的時候,react會再次把它轉回來的。如topInput轉成input之類的。
接着往下走dependencies獲得的是一個依賴事件數組,隨即遍歷這個數組,作一些hack處理,而後會調用這個方法trapBubbledEvent。
trapBubbledEvent(dependency, topLevelTypes[dependency], mountAt) ,三個參數 dependency= topClick。 上面提到過的,會把top前綴什麼的再次轉回來, 因此topLevelTypes[dependency]就是click。mountAt就等於document(不考慮多個window)。那trapBubbledEvent又作了什麼呢?

clipboard.png

這裏說白就是調用下面這個方法

clipboard.png

熟悉了吧。這就是咱們常見的事件綁定了。。
又到劃重點時間了
listenTo作的事情很簡單,就是遍歷props中的event,而後將事件和事件的依賴事件通通掛載到document上,而且全部的事件的回調函數走的都是dispatchEvent。
打個比方,若是我綁定一個onChange事件,那麼react不只僅只綁定一個onChange事件到document上,還會綁定許多依賴事件上去,如focus,blur,input等等。是否是看出點什麼苗頭。onChange事件依賴了onInput事件,但這還並非 爲何onChange的表現形式和onInput同樣的 所有緣由。 這只是爲後續的合成提供了依賴。
整個事件註冊差很少就到這裏爲止了。react會把全部的事件都掛載到document上。這也是爲何咱們event.stoppropagation()爲何不能阻止原生冒泡。由於全部事件都是綁定在document上的。意味着你的原生事件都執行完了以後,才能執行document的事件。dispatchEvent會作統一的派發。能夠說原生事件的執行順序是早於react事件的。

稍微說起下react15的事件註冊和16徹底不一樣,16有一個listenBink的感念,因此的事件都註冊到這個對象裏面,而且由react_id關聯起來。但16已經捨棄了react_id的感念。


react事件合成

以一個input輸入框爲例,當用戶輸入了數字1以後,react作了什麼。
首先回到上面說的disPatchEvent中。全部的事件回調通通都是這個函數,如今咱們看看這個函數作了什麼

clipboard.png

dispatchEvent接收兩個參數 topLevelType(事件topFocus之類的) nativeEvent就是原生的event對象。
dispatchEvent裏面繞得有點深,跳來跳去。最後會走到下面這個方法裏面來

clipboard.png

handleTopLevel接收4個參數,targetInst就是fiber實例,上面提到過每一個dom中都會掛載兩個屬性,其中一個保存了fiber的引用。過程就是根據dispatchEvent獲得的一個nativeEvent,能夠獲得
一個真正觸發事件的nativeEventTarget元素(event.target),而後取得fiber引用便可。 這樣handleTopLevel四個關鍵參數都齊全了。
handleTopLevel又作了什麼呢。。根據裏面方法的字面意思 無非是提取event對象(react的合成對象,並不是原生的event),而後放到事件隊裏去。
重點來講說extractEvents方法,全部的奧祕都在這裏了。。。它接收了handleTopLevel的四個參數。

clipboard.png

這裏可能有點繞,須要理解下plugins。簡單的解釋就是react的event模塊所包含的eventPliguns。好像有6個左右的plugin吧,或許是不一樣的組件處理不一樣的事件類型吧。具體實現和功能就不必說了,太底層。
好比用戶在input輸入的過程,或許第一步是觸發了某個元素的blur,而後是input的focus,而後是keydown,input之類等等,順序就是按照瀏覽器的事件順序。
咱們拿input事件舉例,撇開其餘無關事件(注:這裏會解釋 最開始提到的第一個問題)。

<input onChange= {this.changeText} /> 。

劃重點了。。
上面的Input,雖然咱們只註冊了一個onChange,但根據咱們前面的瞭解,react會註冊依賴事件,onChange會依賴onInput,所以一樣會註冊onInput事件。
當input事件被觸發的時候,遍歷plugin去處理事件。並返回一個由plugin合成的event
events = accumulateInto(events, extractedEvents); 看這裏,events是一個數組,accumulateInto等同於events.push(extractedEvents);
重點來 plugin處理onInput事件的時候,會生成兩個event,一個是input,一個是change, 會生成兩個event,一個是input,一個是change, 會生成兩個event,一個是input,一個是change,
重要的事情說三遍,記住這個地方。後面是關鍵。
回到plugin這裏來,進入到possiblePlugin.extractEvents裏面去,這個函數返回一個event,而且在層層調用後執行了一個相當重要的函數traverseTwoPhase ,讓咱們走進去看看這個函數
究竟作了什麼?

clipboard.png

traverseTwoPhase接收三個參數,inst = fiber實例 fn = accumulateDirectionalDispatches函數,等會會着重講解這個方法。arg = event。這個event是possiblePlugin.extractEvents中生成的event對象,它把這個event當參數傳遞到更深層的方法裏面,是爲了在event上掛載兩個極爲重要的屬性,等下細說。
traverseTwoPhase 具體作了什麼呢?
while循環,取到fiber(觸發事件的真正target所對應的虛擬dom)的全部父節點,等同於獲得了一棵fiberTree。
以下

<ComponentA onClick={() => {console.log(1111)}}>
    <ComponentB onClick={() => {console.log(2222)}}>
        <ComponentC onClick={() => {console.log(3333)}} />
    </ComponentB>
</ComponentA>

假設有上面三個組件ComponentA, ComponentB, ComponentC。層層嵌套。那麼path就等於[ComponentC, ComponentB, ComponentA];
traverseTwoPhase 中執行了兩次循環,一次爲captured捕獲,即執行順序從A-C。一次爲bubbled冒泡,執行順序爲從C-A。這裏咱們不考慮captured。詳細講解下冒泡過程。上面
說過了fn = accumulateDirectionalDispatches,咱們看看這裏到底作了什麼

clipboard.png

重點看這裏 var listener = listenerAtPhase(inst, event, phase); 這裏就是取當前的dom有沒有註冊對應事件的listener。粗略解釋下取得的過程就是利用了最上面說過的綁定在dom
上的props,若是listener存在,則將當前的listener和inst(理解爲虛擬dom或者fiber實例)分別掛載到event下的兩個數組裏。 這裏極其重要
注意啦。 accumulateDirectionalDispatches 這個函數是在 path的循環裏執行的。回到上面的path等於[ComponentC, ComponentB, ComponentA]的例子。由於咱們的ABC三個組件都註冊了對應的listener,故而event下的_dispatchListeners和_dispatchInstances都存儲有其inst和listner。至此event算是合成完畢了,控制權回到possiblePlugin.extractEvents這裏。

劃重點 : 事件合成的過程。首先根據觸發事件的target獲得inst,而後遍歷他的全部父節點(fiber.return屬性),存儲在局部遍歷path中,記住這個path是有順序關係的(後面能夠解釋react事件是如何阻止冒泡的)。獲得path後進行遍歷,假設遍歷的組件一樣註冊了對應事件的listener,那麼就掛載到event的_dispatchListeners和_dispatchInstances中去,這兩個屬性相當重要,後續的事件派發就是根據這兩個屬性進行的。 注意只有註冊了對應事件的listener,纔會掛載到event裏面去。好比剛剛咱們的ABC都綁定了Click,天然都會push到_dispatchListeners中去。

回憶下上面剛剛說到的一個很繞的地方。 假設我給一個input綁定了onChange事件,那麼react會綁定不少依賴事件到document上。其中就有input事件。 但當咱們觸發input的時候,react是怎麼觸發到onChange的listener呢? 。重點就是剛剛說了三遍的地方,在合成input事件的時候,react會生成兩個event,一個是input,一個是change,也就是說change的那個event掛載的_dispatchListeners裏面存儲了咱們的listener。後續派發的時候,會執行這個事件隊列,對 隊列裏的event進行派發。 這也就很好的解釋了爲何咱們的change交互和input如出一轍。


事件派發

事件合成以後就是事件派發了,雖然我分開來說,當兩個過程是緊跟着的,合成事件後,全部的事件都會push到eventQueue裏面去。派發的過程實際上就是遍歷事件隊列的過程。
遍歷的過程一樣有點繞,咱們只看關鍵的地方

clipboard.png

有麼有看到咱們熟悉的地方。_dispatchListeners和_dispatchInstances這兩個裏面保存了咱們的inst和listener。**方法對dispatchListeners進行了遍歷,event.isPropagationStopped()
這個地方也就是咱們能夠阻止合成事件冒泡的緣由呢。。** 由於dispatchListeners一樣是按照冒泡的順序插入的,就拿剛剛的ABC三個組件來講。假設對B進行了阻止冒泡。那麼A的onClick就沒辦法
執行了。
遍歷過程當中拿到inst和對應的listener後,執行executeDispatch,後續的代碼就簡單直接了。

clipboard.png

建立了個虛假的dom,綁定個自定義事件,而後再本身監聽,再本身createEvent,再dispatch,觸發callback,而後在callback裏面觸發真正的listener,同時會把合成的event傳遞進去。
最後在清空重置各類數據。。大結局了。

事件註冊,事件合成,事件派發,三個階段,這裏就再也不總結了,每一階段後都有個總結的。講真react代碼有點複雜,看不到的多看幾遍吧。寫的不對的地方指正下。有不懂的提問。。碼這麼多不容易了,可能有些單詞拼錯了,包容下。

相關文章
相關標籤/搜索