1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        【React】782- React 中阻止事件冒泡的問題

        共 7807字,需瀏覽 16分鐘

         ·

        2020-11-19 06:54

        來源 |?https://www.cnblogs.com/Wayou/p/react_event_issue.html

        在正式開始前,先來看看 js 中事件的觸發(fā)與事件處理器的執(zhí)行。

        js?中事件的監(jiān)聽與處理

        事件捕獲與冒泡

        DOM 事件會先后經(jīng)歷?捕獲?與?冒泡?兩個階段。捕獲即事件沿著 DOM 樹由上往下傳遞,到達觸發(fā)事件的元素后,開始由下往上冒泡。

        IE9 及之前的版本只支持冒泡

        | A
        -----------------|--|-----------------
        | Parent | | |
        | -------------|--|----------- |
        | |Children V | | |
        | ---------------------------- |
        | |
        --------------------------------------

        事件處理器

        默認情況下,事件處理器是在事件的冒泡階段執(zhí)行,無論是直接設置元素的 onclick 屬性還是通過 EventTarget.addEventListener()?來綁定,后者在沒有設置 useCapture 參數(shù)為 true 的情況下。

        考察下面的示例:

        <button onclick="btnClickHandler(event)">CLICK MEbutton><script> document.addEventListener("click", function(event) { console.log("document clicked"); });
        function btnClickHandler(event) { console.log("btn clicked"); }script>

        輸出:

        btn clickeddocument clicked

        阻止事件的冒泡

        通過調(diào)用事件身上的 stopPropagation()?可阻止事件冒泡,這樣可實現(xiàn)只我們想要的元素處理該事件,而其他元素接收不到。

        <button onclick="btnClickHandler(event)">CLICK MEbutton><script> document.addEventListener( "click", function(event) { console.log("document clicked"); }, false );
        function btnClickHandler(event) { event.stopPropagation(); console.log("btn clicked"); }script>

        輸出:

        btn clicked

        一個阻止冒泡的應用場景

        常見的彈窗組件中,點擊彈窗區(qū)域之外關(guān)閉彈窗的功能,可通過阻止事件冒泡來方便地實現(xiàn),而不用這種方式的話,會引入復雜的判斷當前點擊坐標是否在彈窗之外的復雜邏輯。

        document.addEventListener("click", () => { // close dialog});
        dialogElement.addEventListener("click", event => {event.stopPropagation();});

        但如果你嘗試在 react 中實現(xiàn)上面的邏輯,一開始的嘗試會讓你懷疑人生。

        react?下事件執(zhí)行的問題

        了解 JS 中事件的基礎后,會覺得一切都沒什么復雜。但在引入 React 后,事情開始起變化。將上面阻止冒泡的邏輯在 React 里實現(xiàn)一下,代碼大概像這樣:

        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 MEbutton>;}

        輸出:

        btn clickeddocument?clicked

        document 上的事件處理器正常執(zhí)行了,并沒有因為我們在按鈕里面調(diào)用 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 (<divonClick={() => {console.log("raect:container");}}><buttononClick={event => {console.log("react:button");}}>CLICK MEbutton>div>);}
        ReactDOM.render(<App />, document.getElementById("root"));document.addEventListener("click", event => {console.log("document:after react mount");});

        現(xiàn)在對代碼做一些變動,在 body 的事件處理器中把冒泡阻止,再思考其輸出。

        document.body.addEventListener("click", event => {+ event.stopPropagation(); console.log("body");});

        下面是劇透環(huán)節(jié),如果你懶得自己實驗的話。

        點擊按鈕后的輸出:

        bodydocument:bedore react mountreact:buttonraect:containerdocument:after react mountdocument within reactwindow

        bdoy 上阻止冒泡后,你可能會覺得,既然 body 是按鈕及按鈕容器的父級,那么按鈕及容器的事件會正常執(zhí)行,事件到達 body 后, body 的事件處理器執(zhí)行,然后就結(jié)束了。document 上的事件處理器一個也不執(zhí)行。

        事實上,按鈕及按鈕容器上的事件處理器也沒執(zhí)行,只有 body 執(zhí)行了。

        輸出:

        body

        通過下面的分析,你能夠完全理解上面的結(jié)果。

        SyntheticEvent

        React 有自身的一套事件系統(tǒng),叫作 SyntheticEvent。叫什么不重要,實現(xiàn)上,其實就是通過在 document 上注冊事件代理了組件樹中所有的事件(facebook/react#4335),并且它監(jiān)聽的是 document 冒泡階段。你完全可以忽略掉 SyntheticEvent 這個名詞,如果覺得它有點讓事情變得高大上或者增加了一些神秘的話。

        除了事件系統(tǒng),它有自身的一套,另外還需要理解的是,界面上展示的 DOM 與我們代碼中的 DOM 組件,也是兩樣東西,需要在概念上區(qū)分開來。

        所以,當你在頁面上點擊按鈕,事件開始在原生 DOM 上走捕獲冒泡流程。React 監(jiān)聽的是 document 上的冒泡階段。事件冒泡到 document 后,React 將事件再派發(fā)到組件樹中,然后事件開始在組件樹 DOM 中走捕獲冒泡流程。

        現(xiàn)在來嘗試理解一下輸出結(jié)果:

        • 事件最開始從原生 DOM 按鈕一路冒泡到 body,body 的事件處理器執(zhí)行,輸出 body。注意此時流程還沒進入 React。為什么?因為 React 監(jiān)聽的是 document 上的事件。

        • 繼續(xù)往上事件冒泡到 document。

          • 事件到達 document 之后,發(fā)現(xiàn) document 上面一共綁定了三個事件處理器,分別是代碼中通過 document.addEventListener 在 ReactDOM.render 前后調(diào)用的,以及一個隱藏的事件處理器,是 ReactDOM 綁定的,也就是前面提到的 React 用來代理事件的那個處理器。

          • 同一元素上如果對同一類型的事件綁定了多個處理器,會按照綁定的順序來執(zhí)行。

          • 所以 ReactDOM.render 之前的那個處理器先執(zhí)行,輸出 document:before react mount。

          • 然后是 React 的事件處理器。此時,流程才真正進入 React,走進我們的組件。組件里面就好理解了,從 button 冒泡到 container,依次輸出。

          • 最后 ReactDOM.render 之后的那個處理器先執(zhí)行,輸出 document:after react mount。

        • 事件完成了在 document 上的冒泡,往上到了 window,執(zhí)行相應的處理器并輸出 window。

        理解 React 是通過監(jiān)聽 document 冒泡階段來代理組件中的事件,這點很重要。同時,區(qū)分原生 DOM 與 React 組件,也很重要。并且,React 組件上的事件處理器接收到的 event 對象也有別于原生的事件對象,不是同一個東西。但這個對象上有個 nativeEvent 屬性,可獲取到原生的事件對象,后面會用到和討論它。

        緊接著的代碼的改動中,我們在 body 上阻止了事件冒泡,這樣事件在 body 就結(jié)束了,沒有到達 document,那么 React 的事件就不會被觸發(fā),所以 React 組件樹中,按鈕及容器就沒什么反應。如果沒理解到這點,光看表象還以為是 bug。

        進而可以理解,如果在 ReactDOM.render()?之前的的 document 事件處理器上將冒泡結(jié)束掉,同樣會影響 React 的執(zhí)行。只不過這里需要調(diào)用的不是 event.stopPropagation(),而是 event.stopImmediatePropagation()。

        document.addEventListener("click", event => {+ event.stopImmediatePropagation(); console.log("document:bedore react mount");});

        輸出:

        bodydocument:bedore react mount

        stopImmediatePropagation 會產(chǎn)生這樣的效果,即,如果同一元素上同一類型的事件(這里是 click)綁定了多個事件處理器,本來這些處理器會按綁定的先后來執(zhí)行,但如果其中一個調(diào)用了 stopImmediatePropagation,不但會阻止事件冒泡,還會阻止這個元素后續(xù)其他事件處理器的執(zhí)行。

        所以,雖然都是監(jiān)聽 document 上的點擊事件,但 ReactDOM.render()?之前的這個處理器要先于 React,所以 React 對 document 的監(jiān)聽不會觸發(fā)。

        解答前面按鈕未能阻止冒泡的問題

        如果你已經(jīng)忘了,這是相應的代碼及輸出。

        到這里,已經(jīng)可以解答為什么 React 組件中 button 的事件處理器中調(diào)用 event.stopPropagation()?沒有阻止 document 的點擊事件執(zhí)行的問題了。因為 button 事件處理器的執(zhí)行前提是事件達到 document 被 React 接收到,然后 React 將事件派發(fā)到 button 組件。既然在按鈕的事件處理器執(zhí)行之前,事件已經(jīng)達到 document 了,那當然就無法在按鈕的事件處理器進行阻止了。

        問題的解決

        要解決這個問題,這里有不止一種方法。

        用?window?替換?document

        來自?React issue 回答中提供的這個方法是最快速有效的。使用 window 替換掉 document 后,前面的代碼可按期望的方式執(zhí)行。

        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 MEbutton>;}

        這里 button 事件處理器上接到到的 event 來自 React 系統(tǒng),也就是 document 上代理過來的,所以通過它阻止冒泡后,事件到 document 就結(jié)束了,而不會往上到 window。

        Event.stopImmediatePropagation()

        組件中事件處理器接收到的 event 事件對象是 React 包裝后的 SyntheticEvent 事件對象。但可通過它的 nativeEvent 屬性獲取到原生的 DOM 事件對象。通過調(diào)用這個原生的事件對象上的 stopImmediatePropagation()?方法可達到阻止冒泡的目的。

        function btnClickHandler(event) {+ event.nativeEvent.stopImmediatePropagation(); console.log("btn clicked");}

        至于原理,其實前面已經(jīng)有展示過。React 在 render 時監(jiān)聽了 document 冒泡階段的事件,當我們的 App 組件執(zhí)行時,準確地說是渲染完成后(useEffect 渲染完成后執(zhí)行),又在 document 上注冊了 click 的監(jiān)聽。此時 document 上有兩個事件處理器了,并且組件中的這個順序在 React 后面。

        當調(diào)用 event.nativeEvent.stopImmediatePropagation()?后,阻止了 document 上同類型后續(xù)事件處理器的執(zhí)行,達到了想要的效果。

        但這種方式有個缺點很明顯,那就是要求需要被阻止的事件是在 React render 之后綁定,如果在之前綁定,是達不到效果的。

        通過元素自身來綁定事件處理器

        當繞開 React 直接通過調(diào)用元素自己身上的方法來綁定事件時,此時走的是原生 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 ;}

        很明顯這樣是能解決問題,但你根本不會想要這樣做。代碼丑陋,不直觀也不易理解。

        結(jié)論

        注意區(qū)分 React 組件的事件及原生 DOM 事件,一般情況下,盡量使用 React 的事件而不要混用。如果必需要混用比如監(jiān)聽 document,window 上的事件,處理 mousemove,resize 等這些場景,那么就需要注意本文提到的順序問題,不然容易出 bug。

        相關(guān)資源

        • e.stopPropagation() seems to not be working as expect. #4335

        • ReactJS SyntheticEvent stopPropagation() only works with React events?

        • Event.stopImmediatePropagation()

        • SyntheticEvent

        1. JavaScript 重溫系列(22篇全)
        2. ECMAScript 重溫系列(10篇全)
        3. JavaScript設計模式 重溫系列(9篇全)
        4.?正則 / 框架 / 算法等 重溫系列(16篇全)
        5.?Webpack4 入門(上)||?Webpack4 入門(下)
        6.?MobX 入門(上)?||??MobX 入門(下)
        7. 80+篇原創(chuàng)系列匯總

        回復“加群”與大佬們一起交流學習~

        點擊“閱讀原文”查看 80+ 篇原創(chuàng)文章

        瀏覽 39
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        評論
        圖片
        表情
        推薦
        點贊
        評論
        收藏
        分享

        手機掃一掃分享

        分享
        舉報
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            欧美精品天堂 | 91丨人妻丨国产探花 | 全肉高h各种玩具震动男男 | 手机无码在线 | 操逼视频www | 黄色午夜视频 | 好大好爽噼里啪啦的视频 | 国产又粗又长又大又硬又爽 | 国产逼逼 | 成人性生交大免费看A片 |