摘要:二阻止事件冒泡,沒辦法阻止原生事件冒泡。從事件的三個階段講起,會略微提及下的事件機制。就是說注冊某個事件,會強制依賴其他事件。因為所有事件都是綁定在上的。劃重點事件合成的過程。派發的過程實際上就是遍歷事件隊列的過程。
react.js事件機制
寫這篇文章的緣由:一:在給input綁定事件的時候,很好奇為何onChange的交互形式竟然和onInput一模一樣。 因為原生的change事件是在input失去焦點的時候觸發,但react的onChange則完全不同。 二:阻止事件冒泡,event.preventDeault()沒辦法阻止原生事件冒泡。從react事件的三個階段講起,會略微提及下react15的事件機制。
一 事件注冊 二 事件合成 三 事件派發react事件注冊
1. 一切從createInstance,創建dom實例開始講起。
react16引入了fiber的概念,講fiber的文章很多,這里就不多闡述??梢院唵蔚南壤斫鉃閒iber Tree 略微等于 vDOM Tree(當然實際肯定有差別)。
當react遍歷tree創建真實dom實例的時候做了什么???
重點就在這幾行代碼。domElement等于真實創建的dom,里面調用的就是我們熟悉的createElement。而precacheFiberNode和updateFiberProps兩個方法分別給domElement(真實dom)添加了兩個屬性。所有react16項目中的dom都會擁有這兩個屬性,并且這個兩個屬性的屬性名在同一個項目中是一致的。
圖片描述
precacheFiberNode方法中 設置 node[internalInstanceKey] = 一個new FiberNode()的實例
updateFiberProps方法中 設置node[internalEventHandlersKey] = props 。這里的props就是
劃重點了! 這兩個方法等于將真實dom和fiber,props直接關聯到了一起,相互引用,這點很重要,后續會用到,相當于前期準備。
2. listenTo
createInstance后(仍然在fiber tree遍歷中),程序兜兜轉轉,層層調用,最后終于走到了關鍵方法listenTo這里來,所有的事件注冊邏輯都在這里實現。react做了一系列的兼容處理,盡可能的保證各個瀏覽器端交互一致。
ensureListeningTo里面調用的就listenTo。首先去遍歷props中的屬性。而registrationNameModules.hasOwnProperty(propKey),registrationNameModules是一個事件名的集合,幾乎包含了所有的常見事件,這也就是如果你寫一些稀奇古怪的事件,react是不識別的。如果判定props中的屬性 如onClick在registrationNameModules中,并且值typeof === function,則會進入到listenTo中。
回到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又做了什么呢?
這里說白就是調用下面這個方法
熟悉了吧。這就是我們常見的事件綁定了。。
又到劃重點時間了
listenTo做的事情很簡單,就是遍歷props中的event,然后將事件和事件的依賴事件統統掛載到document上,并且所有的事件的回調函數走的都是dispatchEvent。
打個比方,如果我綁定一個onChange事件,那么react不僅僅只綁定一個onChange事件到document上,還會綁定許多依賴事件上去,如focus,blur,input等等。是不是看出點什么苗頭。onChange事件依賴了onInput事件,但這還并不是 為什么onChange的表現形式和onInput一樣的 全部原因。 這只是為后續的合成提供了依賴。
整個事件注冊差不多就到這里為止了。react會把所有的事件都掛載到document上。這也是為什么我們event.stoppropagation()為什么不能阻止原生冒泡。因為所有事件都是綁定在document上的。意味著你的原生事件都執行完了之后,才能執行document的事件。dispatchEvent會做統一的派發??梢哉f原生事件的執行順序是早于react事件的。
稍微提及下react15的事件注冊和16完全不同,16有一個listenBink的感念,所以的事件都注冊到這個對象里面,并且由react_id關聯起來。但16已經舍棄了react_id的感念。
react事件合成以一個input輸入框為例,當用戶輸入了數字1之后,react做了什么。
首先回到上面說的disPatchEvent中。所有的事件回調統統都是這個函數,現在我們看看這個函數做了什么
dispatchEvent接收兩個參數 topLevelType(事件topFocus之類的) nativeEvent就是原生的event對象。
dispatchEvent里面繞得有點深,跳來跳去。最后會走到下面這個方法里面來
handleTopLevel接收4個參數,targetInst就是fiber實例,上面提到過每個dom中都會掛載兩個屬性,其中一個保存了fiber的引用。過程就是根據dispatchEvent得到的一個nativeEvent,可以得到
一個真正觸發事件的nativeEventTarget元素(event.target),然后取得fiber引用即可。 這樣handleTopLevel四個關鍵參數都齊全了。
handleTopLevel又做了什么呢。。根據里面方法的字面意思 無非是提取event對象(react的合成對象,并非原生的event),然后放到事件隊里去。
重點來說說extractEvents方法,所有的奧秘都在這里了。。。它接收了handleTopLevel的四個參數。
這里可能有點繞,需要理解下plugins。簡單的解釋就是react的event模塊所包含的eventPliguns。好像有6個左右的plugin吧,或許是不同的組件處理不同的事件類型吧。具體實現和功能就沒必要說了,太底層。
比如用戶在input輸入的過程,或許第一步是觸發了某個元素的blur,然后是input的focus,然后是keydown,input之類等等,順序就是按照瀏覽器的事件順序。
我們拿input事件舉例,撇開其他無關事件(注:這里會解釋 最開始提到的第一個問題)。
。
劃重點了。。
上面的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 ,讓我們走進去看看這個函數
究竟做了什么?
traverseTwoPhase接收三個參數,inst = fiber實例 fn = accumulateDirectionalDispatches函數,等會會著重講解這個方法。arg = event。這個event是possiblePlugin.extractEvents中生成的event對象,它把這個event當參數傳遞到更深層的方法里面,是為了在event上掛載兩個極為重要的屬性,等下細說。
traverseTwoPhase 具體做了什么呢?
while循環,取到fiber(觸發事件的真正target所對應的虛擬dom)的所有父節點,等同于得到了一棵fiberTree。
如下
{console.log(1111)}}> {console.log(2222)}}> {console.log(3333)}} />
假設有上面三個組件ComponentA, ComponentB, ComponentC。層層嵌套。那么path就等于[ComponentC, ComponentB, ComponentA];
traverseTwoPhase 中執行了兩次循環,一次為captured捕獲,即執行順序從A-C。一次為bubbled冒泡,執行順序為從C-A。這里我們不考慮captured。詳細講解下冒泡過程。上面
說過了fn = accumulateDirectionalDispatches,我們看看這里到底做了什么
重點看這里 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里面去。派發的過程實際上就是遍歷事件隊列的過程。
遍歷的過程同樣有點繞,我們只看關鍵的地方
有么有看到我們熟悉的地方。_dispatchListeners和_dispatchInstances這兩個里面保存了我們的inst和listener。**方法對dispatchListeners進行了遍歷,event.isPropagationStopped()
這個地方也就是我們可以阻止合成事件冒泡的原因呢。。** 因為dispatchListeners同樣是按照冒泡的順序插入的,就拿剛剛的ABC三個組件來說。假設對B進行了阻止冒泡。那么A的onClick就沒辦法
執行了。
遍歷過程中拿到inst和對應的listener后,執行executeDispatch,后續的代碼就簡單直接了。
創建了個虛假的dom,綁定個自定義事件,然后再自己監聽,再自己createEvent,再dispatch,觸發callback,然后在callback里面觸發真正的listener,同時會把合成的event傳遞進去。
最后在清空重置各種數據。。大結局了。
事件注冊,事件合成,事件派發,三個階段,這里就不再總結了,每一階段后都有個總結的。
講真react代碼有點復雜,看不到的多看幾遍吧。
寫的不對的地方指正下。有不懂的提問。。碼這么多不容易了,可能有些單詞拼錯了,包容下。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/107906.html
摘要:前言這是事件機制的第一篇,主要內容有表象理解,驗證,意義和思考。因為合成事件的觸發是基于瀏覽器的事件機制來實現的,通過冒泡機制冒泡到最頂層元素,然后再由統一去處理。合成事件的阻止冒泡不會影響原生事件。 showImg(https://segmentfault.com/img/bVbtvP2?w=800&h=420); 前言 這是 react 事件機制的第一篇,主要內容有:表象理解,驗證...
摘要:前言這是事件機制系列文章的第二篇對于合成的理解,咱們就來說說合成這個名詞。在給注冊事件的時候也是對兼容性做了處理??偨Y以上就是我對于合成這個名詞的理解,其實內部還處理了很多,我只是略微簡單的舉了幾個栗子。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 這是react事件機制系列文章的第二篇-對于合成的理解,...
摘要:對事件機制的初步理解和驗證對于合成的理解事件注冊機制事件執行本文基于進行分析,雖然不是最新版本但是也不會影響我們對事件機制的整體把握和理解。最后希望通過本文可以讓你對事件機制有更清晰的認識和理解。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 寫這個文章也算是實現19年的一個 flag,研究一個知識點并且把...
摘要:文章涉及到的源碼是基于版本,雖然不是最新版本但是也不會影響我們對事件機制的整體把握和理解。到這里事件注冊就完事兒了。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 這是 react 事件機制的第三節 - 事件注冊,通過本文你將了解react 事件的注冊過程,以及在這個過程中主要經過了哪些關鍵步驟,同時結合源...
摘要:文章涉及到的源碼是基于版本,雖然不是最新版本但是也不會影響我們對事件機制的整體把握和理解。總結本文主要是從整體流程上介紹了下事件觸發的過程。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 這是 react 事件機制的第四節-事件執行,一起研究下在這個過程中主要經過了哪些關鍵步驟,本文也是react 事件機制...
閱讀 2902·2021-11-25 09:43
閱讀 2320·2021-11-24 09:39
閱讀 2708·2021-09-23 11:51
閱讀 1400·2021-09-07 10:11
閱讀 1448·2019-08-27 10:52
閱讀 1929·2019-08-26 12:13
閱讀 3356·2019-08-26 11:57
閱讀 1393·2019-08-26 11:31