亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

React中阻止事件冒泡的問題詳析

2019-11-19 11:47:57
字體:
來源:轉載
供稿:網友

前言

最近在研究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 中走捕獲冒泡流程。

現在來嘗試理解一下輸出結果:

  • 事件最開始從原生 DOM 按鈕一路冒泡到 body,body 的事件處理器執行,輸出 body。注意此時流程還沒進入 React。為什么?因為 React 監聽的是 document 上的事件。
  • 繼續往上事件冒泡到 document。
    • 事件到達 document 之后,發現 document 上面一共綁定了三個事件處理器,分別是代碼中通過 document.addEventListener ReactDOM.render 前后調用的,以及一個隱藏的事件處理器,是 ReactDOM 綁定的,也就是前面提到的 React 用來代理事件的那個處理器。
    • 同一元素上如果對同一類型的事件綁定了多個處理器,會按照綁定的順序來執行。
    • 所以 ReactDOM.render 之前的那個處理器先執行,輸出 document:before react mount。
    • 然后是 React 的事件處理器。此時,流程才真正進入 React,走進我們的組件。組件里面就好理解了,從 button 冒泡到 container,依次輸出。
    • 最后 ReactDOM.render 之后的那個處理器先執行,輸出 document:after react mount。
  • 事件完成了在 document 上的冒泡,往上到了 window,執行相應的處理器并輸出 window。

理解 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。

相關資源

總結

以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作具有一定的參考學習價值,謝謝大家對武林網的支持。

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
色综合老司机第九色激情| 欧美又大又硬又粗bbbbb| 精品中文字幕在线观看| 日韩中文字幕免费视频| 色综合五月天导航| 最近中文字幕mv在线一区二区三区四区| 久久久久中文字幕2018| 日韩久久精品成人| 亚洲精品97久久| 亚洲最大在线视频| 亚洲免费av电影| 久久久黄色av| 国产美女精彩久久| 欧美精品videos另类日本| 欧美在线不卡区| 亚洲精品在线不卡| 日韩精品视频免费专区在线播放| 国产91免费看片| 欧美亚洲第一区| 精品国产鲁一鲁一区二区张丽| 欧美激情亚洲一区| 色综久久综合桃花网| 成人免费看黄网站| 中文字幕日韩精品在线观看| 91极品女神在线| 91久久久久久久| 亚洲福利在线播放| 亚洲欧美激情视频| 成人深夜直播免费观看| 日韩免费高清在线观看| 国内精久久久久久久久久人| 久久视频精品在线| 国产91久久婷婷一区二区| 色婷婷综合久久久久| 欧美在线视频导航| 成人激情视频在线播放| 亚洲xxx自由成熟| 7777免费精品视频| 欧美天天综合色影久久精品| 欧美洲成人男女午夜视频| 在线激情影院一区| 国产丝袜精品视频| 亚洲va久久久噜噜噜| 91av视频在线观看| 亚洲精品久久在线| 中日韩美女免费视频网站在线观看| 国产成人精品综合| 国产精品久久久av| 欧美性猛交xxxx免费看漫画| 亚洲一区二区三区毛片| 色在人av网站天堂精品| 国产午夜精品全部视频在线播放| 精品久久久久久久久久久久久| 国产精品电影久久久久电影网| 欧美激情视频一区| 亚洲精品福利视频| 一本色道久久88综合亚洲精品ⅰ| 欧美专区第一页| 精品欧美aⅴ在线网站| 国产日韩综合一区二区性色av| 久久国产精品网站| 欧美大尺度激情区在线播放| 国产精品露脸av在线| 欧美一级视频在线观看| 亚洲肉体裸体xxxx137| 国产日韩欧美91| 久久大大胆人体| 午夜精品久久久久久99热| 久久精品国产69国产精品亚洲| 欧美精品国产精品日韩精品| 欧美激情国产日韩精品一区18| 久久久亚洲福利精品午夜| 8090成年在线看片午夜| 亚洲一区亚洲二区| 欧美国产欧美亚洲国产日韩mv天天看完整| 精品国产电影一区| 亚洲理论片在线观看| 日韩在线观看电影| 国产精品女视频| 国产精品99蜜臀久久不卡二区| 国产精品男女猛烈高潮激情| 欧美精品在线免费| 欧美大片va欧美在线播放| 日韩美女毛茸茸| 伊人久久大香线蕉av一区二区| 欧美性生交xxxxx久久久| 中文字幕亚洲欧美日韩高清| 亚洲国产精品99久久| 国产偷亚洲偷欧美偷精品| 久久久久久97| 亚洲乱码国产乱码精品精天堂| 中文字幕一区二区精品| 精品国产一区二区三区在线观看| 国产精品稀缺呦系列在线| 亚洲影影院av| 日本一区二区在线播放| 伊是香蕉大人久久| 亚洲一区二区三区在线免费观看| 亚洲欧美精品伊人久久| 国产精品永久免费视频| 日韩高清免费观看| 国产精品视频xxxx| 欧美大尺度在线观看| 日韩欧美在线视频观看| 黑人巨大精品欧美一区免费视频| 欧美一区二区视频97| 亚洲精品一区av在线播放| 青青草99啪国产免费| 91中文字幕在线观看| 色婷婷久久av| 亚洲影院污污.| 日韩亚洲在线观看| 日韩欧美中文免费| 国产亚洲免费的视频看| 亚洲韩国青草视频| 欧美麻豆久久久久久中文| 色老头一区二区三区在线观看| 日韩欧美精品在线观看| 日本国产欧美一区二区三区| 国产精品香蕉av| 91精品国产高清久久久久久久久| 最近2019年中文视频免费在线观看| 国产精品高潮视频| 国外日韩电影在线观看| 久久精品在线视频| 国产日韩中文字幕| 国内精品久久久久影院优| 欧美壮男野外gaytube| 欧美日韩成人在线视频| 国产成人一区二区三区小说| 日韩av在线免费观看一区| 久久手机精品视频| 日韩中文第一页| 91老司机精品视频| 国产精品电影网站| 亚洲免费av片| 久久琪琪电影院| 国产精品 欧美在线| 综合网中文字幕| 欧美成人免费小视频| 成人午夜黄色影院| 最近的2019中文字幕免费一页| 亚洲成色www8888| 亚洲精品456在线播放狼人| 正在播放欧美视频| 欧美日韩国产精品一区二区三区四区| 日韩综合视频在线观看| 亚洲国产小视频| 国产精品久久久久久久美男| 成人做爽爽免费视频| 欧美极品美女视频网站在线观看免费| 在线观看国产成人av片| 欧美另类高清videos| 一个色综合导航| 日韩欧美在线免费| 一区三区二区视频| 欧美黄色片在线观看| 亚洲激情小视频| 亚洲人成亚洲人成在线观看| 国产精品第10页| 亚洲黄色免费三级| 国产精品偷伦一区二区| 丝袜美腿亚洲一区二区| 91啪国产在线|