這篇文章要跟大家分享的內容是關於React事件系統的解析,有一定的參考價值,有需要的朋友可以參考一下。
React事件系統有兩類:合成事件和原生事件。
在寫React元件是我們很容易綁定一個合成事件,但是在一個元件裡面是沒有辦法去綁定另一個元件的合成事件的,此時原生事件就派上了用場。
除了講述混合(合成事件與原生事件混用)事件,事件冒泡也是我們經常需要處理的事情,這篇文章結合React進行介紹。
1-1. React 基於Virtual DOM 實作了一個SyntheticEvent(合成事件)層,我們定義的事件處理器會接收到一個SyntheticEvent物件的實例,同樣支援事件的冒泡機制,我們可以使用stopPropagation()和preventDefault()來中斷它。
1-2. 所有事件都會自動綁定到最外層上(document)。
在 React 底層,主要對合成事件做了兩件事:事件委派和自動綁定。
2-1.事件委派在使用
React 事件前,一定要熟悉它的事件代理機制。它不會把事件處理函數直接綁定到真實的節點上,而是把所有事件綁定到結構的最外層,使用一個統一的事件監聽器,這個事件監聽器上維持了一個映射來保存所有元件內部的事件監聽和處理函數。當元件掛載或卸載時,只是在這個統一的事件監聽器上插入或刪除一些物件;當事件發生時,首先被這個統一的事件監聽器處理,然後在映射裡找到真正的事件處理函數並調用。這樣做簡化了事件處理和回收機制,效率也大大提升。
2-2.自動綁定
在React 元件中,每個方法的上下文都會指向該元件的實例,即自動綁定this為目前元件。而且 React 也會對這種引用進行緩存,以達到 CPU 和記憶體的最優化。
React 架構下也可以使用原生事件。 React 提供了完備的生命週期方法,其中componentDidMount會在元件已經完成安裝並且在瀏覽器中存在真實的 DOM 後調用,此時我們就可以完成原生事件的綁定。
但是React不會自動管理原生事件,所以需要你在卸載元件的時候註銷掉原生事件。
書中講到(這裡不做過多介紹):
不要將合成事件與原生事件混用
透過e.target判斷來避免
重點是下面這段話,這也是我們今天要著重解決的問題:
用reactEvent.nativeEvent.stopPropagation()來阻止冒泡是不行的。阻止 React 事件冒泡的行為只能用於 React 合成事件系統中,且沒辦法阻止原生事件的冒泡。反之,在原生事件中的阻止冒泡行為,卻可以阻止 React 合成事件的傳播。
事件冒泡機制:
透過React 綁定的事件,其回呼函數中的event 對象,是經過React 合成的SyntheticEvent,與原生的DOM 事件的event 不是一回事。確切地說,在 React 中,e.nativeEvent 是原生 DOM 事件的那個 event。
React 合成事件與原生事件執行順序圖:
#從圖中我們可以得到結論:
(1)DOM事件冒泡到document上才會觸發React的合成事件,所以React 合成事件物件的e.stopPropagation,只能阻止React 模擬的事件冒泡,並不能阻止真實的DOM 事件冒泡
(2)DOM事件的阻止冒泡也可以阻止合成事件原因是DOM 事件的阻止冒泡使事件不會傳播到document上
(3)當合成事件和DOM 事件都綁定在document上的時候,React的處理是合成事件應該是先放進去的所以會先觸發,在這種情況下,原生事件物件的stopImmediatePropagation能做到阻止進一步觸發document DOM事件
stopImmediatePropagation :如果有多個相同類型事件的事件監聽函數綁定到同一個元素,則當該類型的事件觸發時,它們會按照被添加的順序執行。如果其中某個監聽函式執行了 event.stopImmediatePropagation()方法,則剩下的監聽函式將不會被執行
(1)阻止合成事件間的冒泡,用e.stopPropagation();
(2)阻止合成事件與最外層document上的事件間的冒泡,用e.nativeEvent.stopImmediatePropagation();
(3)阻止合成事件與除最外層document上的原生事件上的冒泡,透過判斷e.target來避免,程式碼如下:
componentDidMount() { document.body.addEventListener('click', e => { if (e.target && e.target.matches('p.code')) { return; } this.setState({ active: false, }); }); }
7-1 事件註冊
事件註冊即在document 節點,將React 事件轉換為DOM 原生事件,並註冊回呼。
// enqueuePutListener 负责事件注册。 // inst:注册事件的 React 组件实例 // registrationName:React 事件,如:onClick、onChange // listener:和事件绑定的 React 回调方法,如:handleClick、handleChange // transaction:React 事务流,不懂没关系,不太影响对事件系统的理解 function enqueuePutListener(inst, registrationName, listener, transaction) { ... ... // doc 为找到的 document 节点 var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 事件注册 listenTo(registrationName, doc); // 事件存储,之后会讲到,即存储事件回调方法 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); }
來看事件註冊的具體程式碼,如何在 document 上綁定 DOM 原生事件。
// 事件注册 // registrationName:React 事件名,如:onClick、onChange // contentDocumentHandle:要将事件绑定到的 DOM 节点 listenTo: function (registrationName, contentDocumentHandle) { // document var mountAt = contentDocumentHandle; // React 事件和绑定在根节点的 topEvent 的转化关系,如:onClick -> topClick var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName]; for (var i = 0; i <p>來看將事件綁定到冒泡階段的具體程式碼:</p><pre class="brush:php;toolbar:false">// 三个参数为 topEvent、原生 DOM Event、Document(挂载节点) trapBubbledEvent: function (topLevelType, handlerBaseName, element) { if (!element) { return null; } return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType)); } // 三个参数为 Document(挂载节点)、原生 DOM Event、事件绑定函数 listen: function listen(target, eventType, callback) { // 去除浏览器兼容部分,留下核心后 target.addEventListener(eventType, callback, false); // 返回一个解绑的函数 return { remove: function remove() { target.removeEventListener(eventType, callback, false); } } }
在 listen 方法中,我們終於發現了熟悉的 addEventListener 這個原生事件註冊方法。只有 document 節點才會呼叫這個方法,故只有 document 節點上才有 DOM 事件。這大大簡化了 DOM 事件邏輯,也節省了記憶體。
7-2.事件儲存
事件註冊之後,還需要將事件綁定的回呼函數儲存下來。這樣,觸發事件後才能去尋找對應回呼來觸發。在一開始的程式碼中,我們已經看到,就是使用 putListener 方法來進行事件回呼儲存。
// inst:注册事件的 React 组件实例 // registrationName:React 事件,如:onClick、onChange // listener:和事件绑定的 React 回调方法,如:handleClick、handleChange putListener: function (inst, registrationName, listener) { // 核心代码如下 // 生成每个组件实例唯一的标识符 key var key = getDictionaryKey(inst); // 获取某种 React 事件在回调存储银行中的对象 var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {}); bankForRegistrationName[key] = listener; }
7-3.事件執行
每次觸發事件都會執行根節點上addEventListener 註冊的回調,也就是ReactEventListener.dispatchEvent 方法,事件分發入口函數。此函數的主要業務邏輯如下:
根據 DOM 事件建構 React 合成事件。
將合成事件放入佇列。
批次佇列中的事件(包含先前未處理完的,先入先處理)
找到事件觸發的DOM 和React Component
從該React Component,呼叫findParent 方法,遍歷得到所有父元件,存在數組中。
從該元件直到最後一個父元件,根據先前事件存儲,用React 事件名稱元件key,找到對應綁定回呼方法,執行,詳細過程為:
React合成事件的冒泡並不是真的冒泡,而是節點的遍歷。
個人覺得stopImmediatePropagation非常有用,很必要阻止合成事件冒泡到DOM document上,原因是:
1.合成事件本来就绑定在document上,完全可以获取这个document 2.stopImmediatePropagation可以阻止触发的document DOM上的事件,这十分有必要 3.不会阻止DOM 上的事件冒泡到document DOM
相關推薦:
#以上是對React事件系統的解析的詳細內容。更多資訊請關注PHP中文網其他相關文章!