主要分爲4大塊兒,主要是結合源碼對 react事件機制的原理
進行分析,但願可讓你對 react事件機制有更清晰的認識和理解。javascript
固然確定會存在一些表述不清或者理解不夠標準的地方,還請各位大神、大佬斧正。html
01 - 對事件機制的初步理解和驗證html5
02 - 對於合成的理解java
03 - 事件註冊機制node
04 - 事件執行機制react
01 02 是理論的廢話,也算是個人我的總結,沒興趣的能夠直接跳到 03-事件執行機制。chrome
ps: 本文基於 react15.6.1進行分析,雖然不是最新版本可是也不會影響咱們對 react 事件機制的總體把握和理解。數組
對 react事件機制
的表象理解,驗證,意義和思考。瀏覽器
先回顧下 對react 事件機制基本理解,react自身實現了一套本身的事件機制,包括事件註冊、事件的合成、事件冒泡、事件派發等,雖然和原生的是兩碼事,但也是基於瀏覽器的事件機制下完成的。緩存
咱們都知道react 的全部事件並無綁定到具體的dom節點上而是綁定在了document 上,而後由統一的事件處理程序來處理,同時也是基於瀏覽器的事件機制(冒泡),全部節點的事件都會在 document 上觸發。
既然已經有了對 react事件
的一個基本的認知,那這個認知是否正確呢?咱們能夠經過簡單的方法進行驗證。
驗證內容:
全部事件均註冊到了元素的最頂層-document 上 節點的事件由統一的入口處理 爲了方便,直接經過 cli 建立一個項目。
componentDidMount(){ document.getElementById('btn-reactandnative').addEventListener('click', (e) => { console.log('原生+react 事件: 原生事件執行'); }); } handleNativeAndReact = (e) => { console.log('原生+react 事件: 當前執行react事件'); } handleClick=(e)=>{ console.log('button click'); } render(){ return <div className="pageIndex"><p>react event!!!</p <button id="btn-confirm" onClick={this.handleClick}>react 事件</button> <button id="btn-reactandnative" onClick={this.handleNativeAndReact}>原生 + react 事件</button> </div> }
代碼中給兩個 button
綁定了合成事件,單獨給 btn#btn-reactandnative
綁定了一個原生的事件。
而後看下 chrome
控制檯,查看元素上註冊的事件。
通過簡單的驗證,能夠看到全部的事件根據不一樣的事件類型都綁定在了 document
上,觸發函數統一是 dispatchEvent
。
若是一個節點上同時綁定了合成和原生事件,那麼禁止冒泡後執行關係是怎樣的呢?
其實讀到這裏答案已經有了。咱們如今基於目前的知識去分析下這個關係。
由於合成事件的觸發是基於瀏覽器的事件機制來實現的,經過冒泡機制冒泡到最頂層元素,而後再由 dispatchEvent
統一去處理。
* 得出的結論:*
原生事件阻止冒泡確定會阻止合成事件的觸發。
合成事件的阻止冒泡不會影響原生事件。
爲何呢?先回憶下瀏覽器事件機制
瀏覽器事件的執行須要通過三個階段,捕獲階段-目標元素階段-冒泡階段。
節點上的原生事件的執行是在目標階段,然而合成事件的執行是在冒泡階段,因此原生事件會先合成事件執行,而後再往父節點冒泡。
既然原生都阻止冒泡了,那合成還執行個啥嘞。
好,輪到合成的被阻止冒泡了,那原生會執行嗎?固然會了。
由於原生的事件先於合成的執行,因此合成事件內阻止的只是合成的事件冒泡。(代碼我就不貼了)
因此得出結論:
原生事件(阻止冒泡)會阻止合成事件的執行
合成事件(阻止冒泡)不會阻止原生事件的執行
二者最好不要混合使用,避免出現一些奇怪的問題
react 本身作這麼多的意義是什麼?
減小內存消耗,提高性能,不須要註冊那麼多的事件了,一種事件類型只在 document 上註冊一次
統一規範,解決 ie 事件兼容問題,簡化事件邏輯
對開發者友好
既然 react 幫咱們作了這麼多事兒,那他的背後的機制是什麼樣的呢?
事件怎麼註冊的,事件怎麼觸發的,冒泡機制怎樣實現的呢?
請繼續日後看......
剛據說合成這個詞時候,感受是特別高大上,頗有深度,不是很好理解。
當我大概的瞭解過react事件機制後,略微瞭解一些皮毛,我以爲合成不僅僅是事件的合成和處理,從廣義上來講還包括:
對原生事件的封裝
對某些原生事件的升級和改造
不一樣瀏覽器事件兼容的處理
上面代碼是給一個元素添加 click
事件的回調方法,方法中的參數 e
,其實不是原生事件對象而是react包裝過的對象,同時原生事件對象被放在了屬性 e.nativeEvent
內。
經過調試,在執行棧裏看下這個參數 e
包含哪些屬性
再看下官方說明文檔
SyntheticEvent是react合成事件的基類,定義了合成事件的基礎公共屬性和方法。
react會根據當前的事件類型來使用不一樣的合成事件對象,好比鼠標單機事件 - SyntheticMouseEvent,焦點事件-SyntheticFocusEvent等,可是都是繼承自SyntheticEvent。
對於有些dom元素事件,咱們進行事件綁定以後,react並非只處理你聲明的事件類型,還會額外的增長一些其餘的事件,幫助咱們提高交互的體驗。
這裏就舉一個例子來講明下:
當咱們給input聲明個onChange事件,看下 react幫咱們作了什麼?
能夠看到react不僅是註冊了一個onchange事件,還註冊了不少其餘事件。
而這個時候咱們向文本框輸入內容的時候,是能夠實時的獲得內容的。
然而原生只註冊一個onchange的話,須要在失去焦點的時候才能觸發這個事件,因此這個原生事件的缺陷react也幫咱們彌補了。
ps: 上面紅色箭頭中有一個 invalid事件,這個並無註冊到document上,而是在具體的元素上。個人理解是這個是html5新增的一個事件,當輸入的數據不符合驗證規則的時候自動觸發,然而驗證規則和配置都要寫在當前input元素上,若是註冊到document上這個事件就無效了。
react在給document註冊事件的時候也是對兼容性作了處理。
上面這個代碼就是給document註冊事件,內部其實也是作了對 ie瀏覽器
的兼容作了處理。
以上就是我對於react合成這個名詞的理解,其實react內部還處理了不少,我只是簡單的舉了幾個栗子,後面開始聊事件註冊和事件派發的機制。
這是 react
事件機制的第三節 - 事件註冊,在這裏你將瞭解 react
事件的註冊過程,以及在這個過程當中主要通過了哪些關鍵步驟,同時結合源碼進行驗證和加強理解。
在這裏並不會說很是細節的內容,而是把大概的流程和原理性的內容進行介紹,作到對總體流程有個認知和理解。
react 事件註冊過程其實主要作了2件事:事件註冊、事件存儲。
a. 事件註冊 - 組件掛載階段,根據組件內的聲明的事件類型-onclick,onchange 等,給 document 上添加事件 -addEventListener,並指定統一的事件處理程序 dispatchEvent。
b. 事件存儲 - 就是把 react 組件內的全部事件統一的存放到一個對象裏,緩存起來,爲了在觸發事件的時候能夠查找到對應的方法去執行。
上面大體說了事件註冊須要完成的兩個目標,那完成目標的過程須要通過哪些關鍵處理呢?
首先 react 拿到將要掛載的組件的虛擬 dom(其實就是 react element 對象),而後處理 react dom
的 props ,判斷屬性內是否有聲明爲事件的屬性,好比 onClick,onChange
,這個時候獲得事件類型 click,change
和對應的事件處理程序 fn
,而後執行後面 3步
a. 完成事件註冊
b. 將 react dom
,事件類型,處理函數 fn
放入數組存儲
c. 組件掛載完成後,處理 b 步驟生成的數組,通過遍歷把事件處理函數存儲到 listenerBank(一個對象)
中
看個最熟悉的代碼,也是咱們平常的寫法
//此處代碼省略 handleFatherClick=()=>{ } handleChildClick=()=>{ } render(){ return <div className="box"> <div className="father" onClick={this.handleFatherClick}> <div className="child" onClick={this.handleChildClick}>child </div> </div> </div> }
通過 babel
編譯後,能夠看到最終調用的方法是 react.createElement
,z並且聲明的事件類型和回調就是個 props
。
react.createElement
執行的結果會返回一個所謂的虛擬 dom (react element object)
ReactDOMComponent
在進行組件加載(mountComponent)、更新(updateComponent)的時候,須要對props進行處理(_updateDOMProperties):
能夠看下 registrationNameModules 的內容,就不細說了,他就是一個內置的常量。
接着上面的代碼執行到了這個方法
enqueuePutListener(this, propKey, nextProp, transaction);
在這個方法裏會進行事件的註冊以及事件的存儲,包括冒泡和捕獲的處理
根據當前的組件實例獲取到最高父級-也就是document,而後執行方法 listenTo
- 也是最關鍵的一個方法,進行事件綁定處理。
源碼文件:ReactBrowerEventEmitter.js
最後執行 EventListener.listen(冒泡)
或者 EventListener.capture(捕獲)
,單看下冒泡的註冊,其實就是 addEventListener
的第三個參數是 false
。
也能夠看到註冊事件的時候也對 ie 瀏覽器作了兼容。
上面沒有看到 dispatchEvent 的定義,下面能夠看到傳入 dispatchEvent 方法的代碼。
到這裏事件註冊就完事兒了。
開始事件的存儲,在 react 裏全部事件的觸發都是經過 dispatchEvent方法統一進行派發的,而不是在註冊的時候直接註冊聲明的回調,來看下如何存儲的 。
react 把全部的事件和事件類型以及react 組件進行關聯,把這個關係保存在了一個 map裏,也就是一個對象裏(鍵值對),而後在事件觸發的時候去根據當前的 組件id和 事件類型查找到對應的 事件fn。
結合源碼:
function enqueuePutListener(inst, registrationName, listener, transaction) { var containerInfo = inst._hostContainerInfo; var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE; var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; listenTo(registrationName, doc);//這個方法上面已說完 //這裏涉及到了事務,事物會在之後的章節再介紹,主要看事件註冊 //下面的代碼是將putListener放入數組,當組件掛載完後會依次執行數組的回調。也就是putListener會依次執行 transaction.getReactMountReady().enqueue(putListener, { inst: inst,//組件實例 registrationName: registrationName,//事件類型 click listener: listener //事件回調 fn }); } function putListener() { var listenerToPut = this; //放入數組,回調隊列 EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); }
大體的流程就是執行完 listenTo(事件註冊),而後執行 putListener 方法進行事件存儲,全部的事件都會存儲到一個對象中 - listenerBank,具體由 EventPluginHub進行管理。
//拿到組件惟一標識 id var getDictionaryKey = function getDictionaryKey(inst) { return '.' + inst._rootNodeID; } putListener: function putListener(inst, registrationName, listener) { //獲得組件 id var key = getDictionaryKey(inst); //獲得listenerBank對象中指定事件類型的對象 var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); //存儲回調 fn bankForRegistrationName[key] = listener; //.... }
listenerBank其實就是一個二級 map,這樣的結構更方便事件的查找。
這裏的組件 id 就是組件的惟一標識,而後和fn進行關聯,在觸發階段就能夠找到相關的事件回調。
看到這個結構是否是很熟悉呢?就是咱們日常使用的 object.
到這裏大體的流程已經說完,是否是感受有點明白又不大明白。
不要緊,再來個詳細的圖,從新理解下。
在事件註冊階段,最終全部的事件和事件類型都會保存到 listenerBank中。
那麼在事件觸發的過程當中上面這個對象有什麼用處呢?
其實就是用來查找事件回調
事件觸發過程總結爲主要下面幾個步驟:
1.進入統一的事件分發函數(dispatchEvent)
2.結合原生事件找到當前節點對應的ReactDOMComponent對象
3.開始 事件的合成
3.1 根據當前事件類型生成指定的合成對象
3.2 封裝原生事件和冒泡機制
3.3 查找當前元素以及他全部父級
3.4 在 listenerBank查找事件回調併合成到 event(合成事件結束)
4.批量處理合成事件內的回調事件(事件觸發完成 end)
舉個栗子
在說具體的流程前,先看一個栗子,後面的分析也是基於這個栗子
handleFatherClick=(e)=>{ console.log('father click'); } handleChildClick=(e)=>{ console.log('child click'); } render(){ return <div className="box"> <div className="father" onClick={this.handleFatherClick}> father <div className="child" onClick={this.handleChildClick}>child </div> </div> </div> }
看到這個熟悉的代碼,咱們就已經知道了執行結果。
當我點擊 child div 的時候,會同時觸發father的事件。
進入統一的事件分發函數 (dispatchEvent)。
當我點擊child div 的時候,這個時候瀏覽器會捕獲到這個事件,而後通過冒泡,事件被冒泡到 document 上,交給統一事件處理函數 dispatchEvent 進行處理。(上一文中咱們已經說過 document 上已經註冊了一個統一的事件處理函數 dispatchEvent)。
結合原生事件找到當前節點對應的 ReactDOMComponent對象,在原生事件對象內已經保留了對應的 ReactDOMComponent實例的引用,應該是在掛載階段就已經保存了。
看下ReactDOMComponent實例的內容
事件的合成,冒泡的處理以及事件回調的查找都是在合成階段完成的。
根據當前事件類型找到對應的合成類,而後進行合成對象的生成
//進行事件合成,根據事件類型得到指定的合成類 var SimpleEventPlugin = { eventTypes: eventTypes, extractEvents: function extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget) { var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType]; //代碼已省略.... var EventConstructor; switch (topLevelType) { //代碼已省略.... case 'topClick'://【這裏有一個不解的地方】 topLevelType = topClick,執行到這裏了,可是這裏沒有作任何操做 if (nativeEvent.button === 2) { return null; } //代碼已省略.... case 'topContextMenu'://而是會執行到這裏,獲取到鼠標合成類 EventConstructor = SyntheticMouseEvent; break; case 'topAnimationEnd': case 'topAnimationIteration': case 'topAnimationStart': EventConstructor = SyntheticAnimationEvent;//動畫類合成事件 break; case 'topWheel': EventConstructor = SyntheticWheelEvent;//鼠標滾輪類合成事件 break; case 'topCopy': case 'topCut': case 'topPaste': EventConstructor = SyntheticClipboardEvent; break; } var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget); EventPropagators.accumulateTwoPhaseDispatches(event); return event;//最終會返回合成的事件對象 }
在這一步會把原生事件對象掛到合成對象的自身,同時增長事件的默認行爲處理和冒泡機制
/** * * @param {obj} dispatchConfig 一個配置對象 包含當前的事件依賴 ["topClick"],冒泡和捕獲事件對應的名稱 bubbled: "onClick",captured: "onClickCapture" * @param {obj} targetInst 組件實例ReactDomComponent * @param {obj} nativeEvent 原生事件對象 * @param {obj} nativeEventTarget 事件源 e.target = div.child */ function SyntheticEvent(dispatchConfig, targetInst, nativeEvent, nativeEventTarget) { this.dispatchConfig = dispatchConfig; this._targetInst = targetInst; this.nativeEvent = nativeEvent;//將原生對象保存到 this.nativeEvent //此處代碼略..... var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false; //處理事件的默認行爲 if (defaultPrevented) { this.isDefaultPrevented = emptyFunction.thatReturnsTrue; } else { this.isDefaultPrevented = emptyFunction.thatReturnsFalse; } //處理事件冒泡 ,thatReturnsFalse 默認返回 false,就是不阻止冒泡 this.isPropagationStopped = emptyFunction.thatReturnsFalse; return this; }
下面是增長的默認行爲和冒泡機制的處理方法,其實就是改變了當前合成對象的屬性值, 調用了方法後屬性值爲 true,就會阻止默認行爲或者冒泡。
//在合成類原型上增長preventDefault和stopPropagation方法 _assign(SyntheticEvent.prototype, { preventDefault: function preventDefault() { // ....略 this.isDefaultPrevented = emptyFunction.thatReturnsTrue; }, stopPropagation: function stopPropagation() { //....略 this.isPropagationStopped = emptyFunction.thatReturnsTrue; } );
看下 emptyFunction 代碼就明白了
根據當前節點實例查找他的全部父級實例存入path
/** * * @param {obj} inst 當前節點實例 * @param {function} fn 處理方法 * @param {obj} arg 合成事件對象 */ function traverseTwoPhase(inst, fn, arg) { var path = [];//存放全部實例 ReactDOMComponent while (inst) { path.push(inst); inst = inst._hostParent;//層級關係 } var i; for (i = path.length; i-- > 0;) { fn(path[i], 'captured', arg);//處理捕獲 ,反向處理數組 } for (i = 0; i < path.length; i++) { fn(path[i], 'bubbled', arg);//處理冒泡,從0開始處理,咱們直接看冒泡 } }
看下 path 長啥樣
在listenerBank查找事件回調併合成到 event。
緊接着上面代碼
fn(path[i], 'bubbled', arg);
上面的代碼會調用下面這個方法,在 listenerBank
中查找到事件回調,並存入合成事件對象。
/**EventPropagators.js * 查找事件回調後,把實例和回調保存到合成對象內 * @param {obj} inst 組件實例 * @param {string} phase 事件類型 * @param {obj} event 合成事件對象 */ function accumulateDirectionalDispatches(inst, phase, event) { var listener = listenerAtPhase(inst, event, phase); if (listener) {//若是找到了事件回調,則保存起來 (保存在了合成事件對象內) event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);//把事件回調進行合併返回一個新數組 event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);//把組件實例進行合併返回一個新數組 } } /** * EventPropagators.js * 中間調用方法 拿到實例的回調方法 * @param {obj} inst 實例 * @param {obj} event 合成事件對象 * @param {string} propagationPhase 名稱,捕獲capture仍是冒泡bubbled */ function listenerAtPhase(inst, event, propagationPhase) { var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase]; return getListener(inst, registrationName); } /**EventPluginHub.js * 拿到實例的回調方法 * @param {obj} inst 組件實例 * @param {string} registrationName Name of listener (e.g. `onClick`). * @return {?function} 返回回調方法 */ getListener: function getListener(inst, registrationName) { var bankForRegistrationName = listenerBank[registrationName]; if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) { return null; } var key = getDictionaryKey(inst); return bankForRegistrationName && bankForRegistrationName[key]; }
爲何可以查找到的呢?
由於 inst (組件實例)裏有_rootNodeID,因此也就有了對應關係。
到這裏事件合成對象生成完成,全部的事件回調已保存到了合成對象中。
批量處理合成事件對象內的回調方法(事件觸發完成 end)。
生成完 合成事件對象後,調用棧回到了咱們起初執行的方法內。
//在這裏執行事件的回調 runEventQueueInBatch(events);
到下面這一步中間省略了一些代碼,只貼出主要的代碼,下面方法會循環處理 合成事件內的回調方法,同時判斷是否禁止事件冒泡。
貼上最後的執行回調方法的代碼
/** * * @param {obj} event 合成事件對象 * @param {boolean} simulated false * @param {fn} listener 事件回調 * @param {obj} inst 組件實例 */ function executeDispatch(event, simulated, listener, inst) { var type = event.type || 'unknown-event'; event.currentTarget = EventPluginUtils.getNodeFromInstance(inst); if (simulated) {//調試環境的值爲 false,按說生產環境是 true //方法的內容請往下看 ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event); } else { //方法的內容請往下看 ReactErrorUtils.invokeGuardedCallback(type, listener, event); } event.currentTarget = null; } /** ReactErrorUtils.js * @param {String} name of the guard to use for logging or debugging * @param {Function} func The function to invoke * @param {*} a First argument * @param {*} b Second argument */ var caughtError = null; function invokeGuardedCallback(name, func, a) { try { func(a);//直接執行回調方法 } catch (x) { if (caughtError === null) { caughtError = x; } } } var ReactErrorUtils = { invokeGuardedCallback: invokeGuardedCallback, invokeGuardedCallbackWithCatch: invokeGuardedCallback, rethrowCaughtError: function rethrowCaughtError() { if (caughtError) { var error = caughtError; caughtError = null; throw error; } } }; if (process.env.NODE_ENV !== 'production') {//非生產環境會經過自定義事件去觸發回調 if (typeof window !== 'undefined' && typeof window.dispatchEvent === 'function' && typeof document !== 'undefined' && typeof document.createEvent === 'function') { var fakeNode = document.createElement('react'); ReactErrorUtils.invokeGuardedCallback = function (name, func, a) { var boundFunc = func.bind(null, a); var evtType = 'react-' + name; fakeNode.addEventListener(evtType, boundFunc, false); var evt = document.createEvent('Event'); evt.initEvent(evtType, false, false); fakeNode.dispatchEvent(evt); fakeNode.removeEventListener(evtType, boundFunc, false); }; } }
最後react 經過生成了一個臨時節點fakeNode,而後爲這個臨時元素綁定事件處理程序,而後建立自定義事件 Event,經過fakeNode.dispatchEvent方法來觸發事件,而且觸發完畢以後當即移除監聽事件。
到這裏事件回調已經執行完成,可是也有些疑問,爲何在非生產環境須要經過自定義事件來執行回調方法。能夠看下上面的代碼在非生產環境對 ReactErrorUtils.invokeGuardedCallback
方法進行了重寫。
主要是從總體流程上介紹了下 react事件的原理,其中並無深刻到源碼的各個細節,包括事務處理、合成的細節等,另外梳理過程當中本身也有一些疑惑的地方,感受說原理還能比較容易理解一些,可是一結合源碼來寫就會以爲亂,由於 react代碼過於龐大,並且盤根錯節,很難抽離,對源碼有興趣的小夥兒能夠深刻研究下,固然仍是但願本文可以帶給你一些啓發,若文章有表述不清或有問題的地方歡迎留言、 交流、斧正。
https://zhuanlan.zhihu.com/p/35468208
https://react.docschina.org/docs/events.html
回覆「加羣」與大佬們一塊兒交流學習~