您好,登錄后才能下訂單哦!
前言
最近在研究react、redux等,網上找了很久都沒有完整的答案,索性自己整理下,這篇文章就來給大家介紹了關于React阻止事件冒泡的相關內容,下面話不多說了,來一起看看詳細的介紹吧
在正式開始前,先來看看 JS 中事件的觸發與事件處理器的執行。
JS 中事件的監聽與處理
事件捕獲與冒泡
DOM 事件會先后經歷 捕獲 與 冒泡 兩個階段。捕獲即事件沿著 DOM 樹由上往下傳遞,到達觸發事件的元素后,開始由下往上冒泡。
IE9 及之前的版本只支持冒泡
| A
-----------------|--|-----------------
| Parent | | |
| -------------|--|----------- |
| |Children V | | |
| ---------------------------- |
| |
--------------------------------------
事件處理器
默認情況下,事件處理器是在事件的冒泡階段執行,無論是直接設置元素的 onclick 屬性還是通過 EventTarget.addEventListener()
來綁定,后者在沒有設置 useCapture 參數為 true 的情況下。
考察下面的示例:
<button onclick="btnClickHandler(event)">CLICK ME</button> <script> document.addEventListener("click", function(event) { console.log("document clicked"); }); function btnClickHandler(event) { console.log("btn clicked"); } </script>
輸出:
btn clicked
document clicked
阻止事件的冒泡
通過調用事件身上的 stopPropagation()
可阻止事件冒泡,這樣可實現只我們想要的元素處理該事件,而其他元素接收不到。
<button onclick="btnClickHandler(event)">CLICK ME</button> <script> document.addEventListener( "click", function(event) { console.log("document clicked"); }, false ); function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); } </script>
輸出:
btn clicked
一個阻止冒泡的應用場景
常見的彈窗組件中,點擊彈窗區域之外關閉彈窗的功能,可通過阻止事件冒泡來方便地實現,而不用這種方式的話,會引入復雜的判斷當前點擊坐標是否在彈窗之外的復雜邏輯。
document.addEventListener("click", () => { // close dialog }); dialogElement.addEventListener("click", event => { event.stopPropagation(); });
但如果你嘗試在 React 中實現上面的邏輯,一開始的嘗試會讓你懷疑人生。
React 下事件執行的問題
了解了 JS 中事件的基礎,一切都沒什么難的。在引入 React 后,,事情開始起變化。將上面阻止冒泡的邏輯在 React 里實現一下,代碼大概像這樣:
function App() { useEffect(() => { document.addEventListener("click", documentClickHandler); return () => { document.removeEventListener("click", documentClickHandler); }; }, []); function documentClickHandler() { console.log("document clicked"); } function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); } return <button onClick={btnClickHandler}>CLICK ME</button>; }
輸出:
btn clicked
document clicked
document 上的事件處理器正常執行了,并沒有因為我們在按鈕里面調用 event.stopPropagation() 而阻止。
那么問題出在哪?
React 中事件處理的原理
考慮下面的示例代碼并思考點擊按鈕后的輸出。
import React, { useEffect } from "react"; import ReactDOM from "react-dom"; window.addEventListener("click", event => { console.log("window"); }); document.addEventListener("click", event => { console.log("document:bedore react mount"); }); document.body.addEventListener("click", event => { console.log("body"); }); function App() { function documentHandler() { console.log("document within react"); } useEffect(() => { document.addEventListener("click", documentHandler); return () => { document.removeEventListener("click", documentHandler); }; }, []); return ( <div onClick={() => { console.log("raect:container"); }} > <button onClick={event => { console.log("react:button"); }} > CLICK ME </button> </div> ); } ReactDOM.render(<App />, document.getElementById("root")); document.addEventListener("click", event => { console.log("document:after react mount"); });
現在對代碼做一些變動,在 body 的事件處理器中把冒泡阻止,再思考其輸出。
document.body.addEventListener("click", event => { + event.stopPropagation(); console.log("body"); });
下面是劇透環節,如果你懶得自己實驗的話。
點擊按鈕后的輸出:
body
document:bedore react mount
react:button
raect:container
document:after react mount
document within react
window
bdoy 上阻止冒泡后,你可能會覺得,既然 body 是按鈕及按鈕容器的父級,那么按鈕及容器的事件會正常執行,事件到達 body 后, body 的事件處理器執行,然后就結束了。 document 上的事件處理器一個也不執行。
事實上,按鈕及按鈕容器上的事件處理器也沒執行,只有 body 執行了。
輸出:
body
通過下面的分析,你能夠完全理解上面的結果。
SyntheticEvent
React 有自身的一套事件系統,叫作 SyntheticEvent。叫什么不重要,實現上,其實就是通過在 document 上注冊事件代理了組件樹中所有的事件(facebook/react#4335),并且它監聽的是 document 冒泡階段。你完全可以忽略掉 SyntheticEvent 這個名詞,如果覺得它有點讓事情變得高大上或者增加了一些神秘的話。
除了事件系統,它有自身的一套,另外還需要理解的是,界面上展示的 DOM 與我們代碼中的 DOM 組件,也是兩樣東西,需要在概念上區分開來。
所以,當你在頁面上點擊按鈕,事件開始在原生 DOM 上走捕獲冒泡流程。React 監聽的是 document 上的冒泡階段。事件冒泡到 document 后,React 將事件再派發到組件樹中,然后事件開始在組件樹 DOM 中走捕獲冒泡流程。
現在來嘗試理解一下輸出結果:
document.addEventListener
在 ReactDOM.render
前后調用的,以及一個隱藏的事件處理器,是 ReactDOM 綁定的,也就是前面提到的 React 用來代理事件的那個處理器。document:before react mount
。ReactDOM.render
之后的那個處理器先執行,輸出 document:after react mount
。理解 React 是通過監聽 document 冒泡階段來代理組件中的事件,這點很重要。同時,區分原生 DOM 與 React 組件,也很重要。并且,React 組件上的事件處理器接收到的 event 對象也有別于原生的事件對象,不是同一個東西。但這個對象上有個 nativeEvent 屬性,可獲取到原生的事件對象,后面會用到和討論它。
緊接著的代碼的改動中,我們在 body 上阻止了事件冒泡,這樣事件在 body 就結束了,沒有到達 document,那么 React 的事件就不會被觸發,所以 React 組件樹中,按鈕及容器就沒什么反應。如果沒理解到這點,光看表象還以為是 bug。
進而可以理解,如果在 ReactDOM.render()
之前的的 document 事件處理器上將冒泡結束掉,同樣會影響 React 的執行。只不過這里需要調用的不是 event.stopPropagation()
,而是 event.stopImmediatePropagation()
。
document.addEventListener("click", event => { + event.stopImmediatePropagation(); console.log("document:bedore react mount"); });
輸出:
body
document:bedore react mount
stopImmediatePropagation 會產生這樣的效果,即,如果同一元素上同一類型的事件(這里是 click)綁定了多個事件處理器,本來這些處理器會按綁定的先后來執行,但如果其中一個調用了 stopImmediatePropagation,不但會阻止事件冒泡,還會阻止這個元素后續其他事件處理器的執行。
所以,雖然都是監聽 document 上的點擊事件,但 ReactDOM.render()
之前的這個處理器要先于 React,所以 React 對 document 的監聽不會觸發。
解答前面按鈕未能阻止冒泡的問題
如果你已經忘了,這是相應的代碼及輸出。
到這里,已經可以解答為什么 React 組件中 button 的事件處理器中調用 event.stopPropagation() 沒有阻止 document 的點擊事件執行的問題了。因為 button 事件處理器的執行前提是事件達到 document 被 React 接收到,然后 React 將事件派發到 button 組件。既然在按鈕的事件處理器執行之前,事件已經達到 document 了,那當然就無法在按鈕的事件處理器進行阻止了。
問題的解決
要解決這個問題,這里有不止一種方法。
用 window 替換 document
來自 React issue 回答中提供的這個方法是最快速有效的。使用 window 替換掉 document 后,前面的代碼可按期望的方式執行。
function App() { useEffect(() => { + window.addEventListener("click", documentClickHandler); return () => { + window.removeEventListener("click", documentClickHandler); }; }, []); function documentClickHandler() { console.log("document clicked"); } function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); } return <button onClick={btnClickHandler}>CLICK ME</button>; }
這里 button 事件處理器上接到到的 event 來自 React 系統,也就是 document 上代理過來的,所以通過它阻止冒泡后,事件到 document 就結束了,而不會往上到 window。
Event.stopImmediatePropagation()
組件中事件處理器接收到的 event 事件對象是 React 包裝后的 SyntheticEvent 事件對象。但可通過它的 nativeEvent 屬性獲取到原生的 DOM 事件對象。通過調用這個原生的事件對象上的 stopImmediatePropagation()
方法可達到阻止冒泡的目的。
function btnClickHandler(event) { + event.nativeEvent.stopImmediatePropagation(); console.log("btn clicked"); }
至于原理,其實前面已經有展示過。React 在 render 時監聽了 document 冒泡階段的事件,當我們的 App 組件執行時,準確地說是渲染完成后(useEffect 渲染完成后執行),又在 document 上注冊了 click 的監聽。此時 document 上有兩個事件處理器了,并且組件中的這個順序在 React 后面。
當調用 event.nativeEvent.stopImmediatePropagation()
后,阻止了 document 上同類型后續事件處理器的執行,達到了想要的效果。
但這種方式有個缺點很明顯,那就是要求需要被阻止的事件是在 React render 之后綁定,如果在之前綁定,是達不到效果的。
通過元素自身來綁定事件處理器
當繞開 React 直接通過調用元素自己身上的方法來綁定事件時,此時走的是原生 DOM 的流程,都沒在 React 的流程里面。
function App() { const btnElement = useRef(null); useEffect(() => { document.addEventListener("click", documentClickHandler); if (btnElement.current) { btnElement.current.addEventListener("click", btnClickHandler); } return () => { document.removeEventListener("click", documentClickHandler); if (btnElement.current) { btnElement.current.removeEventListener("click", btnClickHandler); } }; }, []); function documentClickHandler() { console.log("document clicked"); } function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); } return <button ref={btnElement}>CLICK ME</button>; }
很明顯這樣是能解決問題,但你根本不會想要這樣做。代碼丑陋,不直觀也不易理解。
結論
注意區分 React 組件的事件及原生 DOM 事件,一般情況下,盡量使用 React 的事件而不要混用。如果必需要混用比如監聽 document,window 上的事件,處理 mousemove,resize 等這些場景,那么就需要注意本文提到的順序問題,不然容易出 bug。
相關資源
總結
以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對億速云的支持。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。