摘要:文章涉及到的源碼是基于版本,雖然不是最新版本但是也不會影響我們對事件機制的整體把握和理解。總結本文主要是從整體流程上介紹了下事件觸發的過程。
前言
這是 react 事件機制的第四節-事件執行,一起研究下在這個過程中主要經過了哪些關鍵步驟,本文也是react 事件機制的完結篇,希望本文可以讓你對 react 事件執行的原理有一定的理解。
文章涉及到的源碼是基于 react15.6.1版本,雖然不是最新版本但是也不會影響我們對 react 事件機制的整體把握和理解。
回顧先簡單的回顧下上一文,事件注冊的結果是是把所有的事件回調保存到了一個對象中
那么在事件觸發的過程中上面這個對象有什么用處呢?
其實就是用來查找事件回調。
內容大綱按照我的理解,事件觸發過程總結為主要下面幾個步驟
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}fatherchild
看到這個熟悉的代碼,我們就已經知道了執行結果。
當我點擊 child div 的時候,會同時觸發father的事件。
1、進入統一的事件分發函數 (dispatchEvent)當我點擊child div 的時候,這個時候瀏覽器會捕獲到這個事件,然后經過冒泡,事件被冒泡到 document 上,交給統一事件處理函數 dispatchEvent 進行處理。(上一文中我們已經說過 document 上已經注冊了一個統一的事件處理函數 dispatchEvent)
2、結合原生事件找到當前節點對應的ReactDOMComponent對象在原生事件對象內已經保留了對應的ReactDOMComponent實例,應該是在掛載階段就已經保存了
看下ReactDOMComponent實例的內容
3、開始進行事件合成事件的合成,冒泡的處理以及事件回調的查找都是在合成階段完成的。
3.1 根據當前事件類型找到對應的合成類,然后進行合成對象的生成
//進行事件合成,根據事件類型獲得指定的合成類 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;//最終會返回合成的事件對象 }
3.2 封裝原生事件和冒泡機制
在這一步會把原生事件對象掛到合成對象的自身,同時增加事件的默認行為處理和冒泡機制
/** * * @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 代碼就明白了
3.3 根據當前節點實例查找他的所有父級實例存入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 長啥樣
3.4 在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,所以也就有了對應關系
到這里事件合成對象生成完成,所有的事件回調已保存到了合成對象中。
4、 批量處理合成事件對象內的回調方法(事件觸發完成 end)
第3步生成完 合成事件對象后,調用棧回到了我們起初執行的方法內
//在這里執行事件的回調 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 方法進行了重寫。
5、總結本文主要是從整體流程上介紹了下 react 事件觸發的過程。
主要流程有:
進入統一的事件分發函數(dispatchEvent)
結合原生事件找到當前節點對應的ReactDOMComponent對象
進行事件的合成
3.1 根據當前事件類型生成指定的合成對象
3.2 封裝原生事件和冒泡機制
3.3 查找當前節點以及他的所有父級
3.4 在listenerBank查找事件回調并合成到 event(事件合成結束)
4.批量處理合成事件內的回調事件(事件觸發完成 end)
其中并沒有深入到源碼的細節,包括事務處理、合成的細節等,另外梳理過程中自己也有一些疑惑的地方,對源碼有興趣的小伙兒可以深入研究下,當然還是希望本文能夠帶給你一些啟發,若文章有表述不清或有問題的地方歡迎留言交流。
更多精彩內容歡迎關注我的公眾號 - 前端張大胖
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104429.html
摘要:前言這是事件機制的第一篇,主要內容有表象理解,驗證,意義和思考。因為合成事件的觸發是基于瀏覽器的事件機制來實現的,通過冒泡機制冒泡到最頂層元素,然后再由統一去處理。合成事件的阻止冒泡不會影響原生事件。 showImg(https://segmentfault.com/img/bVbtvP2?w=800&h=420); 前言 這是 react 事件機制的第一篇,主要內容有:表象理解,驗證...
摘要:文章涉及到的源碼是基于版本,雖然不是最新版本但是也不會影響我們對事件機制的整體把握和理解。到這里事件注冊就完事兒了。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 這是 react 事件機制的第三節 - 事件注冊,通過本文你將了解react 事件的注冊過程,以及在這個過程中主要經過了哪些關鍵步驟,同時結合源...
摘要:前言這是事件機制系列文章的第二篇對于合成的理解,咱們就來說說合成這個名詞。在給注冊事件的時候也是對兼容性做了處理。總結以上就是我對于合成這個名詞的理解,其實內部還處理了很多,我只是略微簡單的舉了幾個栗子。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 這是react事件機制系列文章的第二篇-對于合成的理解,...
摘要:對事件機制的初步理解和驗證對于合成的理解事件注冊機制事件執行本文基于進行分析,雖然不是最新版本但是也不會影響我們對事件機制的整體把握和理解。最后希望通過本文可以讓你對事件機制有更清晰的認識和理解。 showImg(https://segmentfault.com/img/bVbtvI3?w=1048&h=550); 前言 寫這個文章也算是實現19年的一個 flag,研究一個知識點并且把...
摘要:整理收藏一些優秀的文章及大佬博客留著慢慢學習原文協作規范中文技術文檔協作規范阮一峰編程風格凹凸實驗室前端代碼規范風格指南這一次,徹底弄懂執行機制一次弄懂徹底解決此類面試問題瀏覽器與的事件循環有何區別筆試題事件循環機制異步編程理解的異步 better-learning 整理收藏一些優秀的文章及大佬博客留著慢慢學習 原文:https://www.ahwgs.cn/youxiuwenzhan...
閱讀 2947·2023-04-25 22:16
閱讀 2092·2021-10-11 11:11
閱讀 3247·2019-08-29 13:26
閱讀 593·2019-08-29 12:32
閱讀 3409·2019-08-26 11:49
閱讀 2987·2019-08-26 10:30
閱讀 1938·2019-08-23 17:59
閱讀 1507·2019-08-23 17:57