React 中阻止事件冒泡的問題
在正式開始前,先來看看 JS 中事件的觸發與事件處理器的執行。 JS 中事件的監聽與處理事件捕獲與冒泡DOM 事件會先後經歷 捕獲 與 冒泡 兩個階段。捕獲即事件沿著 DOM 樹由上往下傳遞,到達觸發事件的元素後,開始由下往上冒泡。
事件處理器默認情況下,事件處理器是在事件的冒泡階段執行,無論是直接設置元素的 考察下面的示例: <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>
輸出:
阻止事件的冒泡通過調用事件身上的 <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>
輸出:
一個阻止冒泡的應用場景常見的彈窗組件中,點擊彈窗區域之外關閉彈窗的功能,可通過阻止事件冒泡來方便地實現,而不用這種方式的話,會引入復雜的判斷當前點擊坐標是否在彈窗之外的復雜邏輯。 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>;
}
輸出:
document 上的事件處理器正常執行了,並沒有因為我們在按鈕裏面調用 那麽問題出在哪? 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");
});
下面是劇透環節,如果你懶得自己實驗的話。 點擊按鈕後的輸出:
bdoy 上阻止冒泡後,你可能會覺得,既然 body 是按鈕及按鈕容器的父級,那麽按鈕及容器的事件會正常執行,事件到達 body 後, body 的事件處理器執行,然後就結束了。 document 上的事件處理器一個也不執行。 事實上,按鈕及按鈕容器上的事件處理器也沒執行,只有 body 執行了。 輸出:
通過下面的分析,你能夠完全理解上面的結果。 SyntheticEventReact 有自身的一套事件系統,叫作 SyntheticEvent。叫什麽不重要,實現上,其實就是通過在 document 上註冊事件代理了組件樹中所有的事件(facebook/react#4335),並且它監聽的是 document 冒泡階段。你完全可以忽略掉 SyntheticEvent 這個名詞,如果覺得它有點讓事情變得高大上或者增加了一些神秘的話。 除了事件系統,它有自身的一套,另外還需要理解的是,界面上展示的 DOM 與我們代碼中的 DOM 組件,也是兩樣東西,需要在概念上區分開來。 所以,當你在頁面上點擊按鈕,事件開始在原生 DOM 上走捕獲冒泡流程。React 監聽的是 document 上的冒泡階段。事件冒泡到 document 後,React 將事件再派發到組件樹中,然後事件開始在組件樹 DOM 中走捕獲冒泡流程。 現在來嘗試理解一下輸出結果:
理解 React 是通過監聽 document 冒泡階段來代理組件中的事件,這點很重要。同時,區分原生 DOM 與 React 組件,也很重要。並且,React 組件上的事件處理器接收到的 緊接著的代碼的改動中,我們在 body 上阻止了事件冒泡,這樣事件在 body 就結束了,沒有到達 document,那麽 React 的事件就不會被觸發,所以 React 組件樹中,按鈕及容器就沒什麽反應。如果沒理解到這點,光看表象還以為是 bug。 進而可以理解,如果在 document.addEventListener("click", event => {
+ event.stopImmediatePropagation();
console.log("document:bedore react mount");
});
輸出:
所以,雖然都是監聽 document 上的點擊事件,但 解答前面按鈕未能阻止冒泡的問題如果你已經忘了,這是相應的代碼及輸出。到這裏,已經可以解答為什麽 React 組件中 button 的事件處理器中調用 問題的解決要解決這個問題,這裏有不止一種方法。 用
|
React 中阻止事件冒泡的問題