React 合成事件

背景:本文是筆者在使用 React 開發過程中遇到的問題,對 React 合成事件做了一些簡單的探索總結,本文不涉及具體的源碼部分,而是通過一些 Demo 去理解 React 合成事件,希望能對大家實際開發過程中有一些幫助。如有語義描述不清或錯誤點,還望大家能夠指出。

大家都知道 React 有一套自己的事件機制,如官方文檔所述:

SyntheticEvent 實例將被傳遞給你的事件處理函數,它是瀏覽器的原生事件的跨瀏覽器包裝器。除兼容所有瀏覽器外,它還擁有和瀏覽器原生事件相同的接口,包括 stopPropagation() 和 preventDefault()。

如果因爲某些原因,當你需要使用瀏覽器的底層事件時,只需要使用 nativeEvent 屬性來獲取即可。合成事件與瀏覽器的原生事件不同,也不會直接映射到原生事件。例如,在 onMouseLeave 事件中 event.nativeEvent 將指向 mouseout 事件。每個 SyntheticEvent 對象都包含以下屬性:

boolean bubbles
boolean cancelable
DOMEventTarget currentTarget
boolean defaultPrevented
number eventPhase
boolean isTrusted
DOMEvent nativeEvent
void preventDefault()
boolean isDefaultPrevented()
void stopPropagation()
boolean isPropagationStopped()
void persist()
DOMEventTarget target
number timeStamp
string type

React 使用合成事件的原因:

下面對比 DOM2 原生事件,演示幾個 Demo 方便更好的理解,Demo 使用 "react": "^16.14.0" 來演示

父組件監聽兩個原生事件

export default class App extends Component {
  componentDidMount() {
    document.addEventListener("click"() ={
      console.log("document");
    });
    window.addEventListener("click"() ={
      console.log("window");
    });
  }
  render() {
    return (
      <div class>
        <Demo />
      </div>
    );
  }
}

1、原生事件

import { Component } from "react";
import "./index.css";
const getDom = (className) ={
  return document.querySelector(`.${className}`);
};
// 冒泡/捕獲
const BUBBLING = false;
export default class Demo extends Component {
  state = {};
  componentDidMount() {
    const c1 = getDom("demo1-content1");
    const c2 = getDom("demo1-content2");
    const c3 = getDom("demo1-content3");
    c1.addEventListener(
      "click",
      () ={
        console.log(1);
      },
      BUBBLING
    );

    c2.addEventListener(
      "click",
      () ={
        console.log(2);
      },
      BUBBLING
    );

    c3.addEventListener(
      "click",
      () ={
        console.log(3);
      },
      BUBBLING
    );
  }
  render() {
    return (
      <div class {...this.props}>
        <h3
          onClick={() ={
            this.setState({ show: !this.state.show });
          }}
          id="black-1"
        >
          demo1 原生事件
        </h3>
        <div class>
          1
          <div class>
            2<div class>3</div>
          </div>
        </div>
        {this.state.show && <p>3-2-1-document-window</p>}
      </div>
    );
  }
}

輸出結果是顯而易見的,如圖:

2、合成事件

import { Component } from "react";
import "./index.css";
export default class Demo extends Component {
  state = {};
  render() {
    return (
      <div class>
        <h3
          onClick={() ={
            this.setState({ show: !this.state.show });
          }}
        >
          demo2 合成事件
        </h3>
        <div
          class
          onClick={() ={
            console.log(1);
          }}
        >
          1
          <div
            class
            onClick={() ={
              console.log(2);
            }}
          >
            2
            <div
              class
              onClick={() ={
                console.log(3);
              }}
            >
              3
            </div>
          </div>
        </div>
        {this.state.show && <p>3-2-1-document-window</p>}
      </div>
    );
  }
}

輸出結果:

3、原生事件和合成事件混合使用

import { Component } from "react";
import "./index.css";
const getDom = (className) ={
  return document.querySelector(`.${className}`);
};
// 冒泡/捕獲
const BUBBLING = false;
export default class Demo extends Component {
  state = {};
  componentDidMount() {
    const c1 = getDom("demo3-content1");
    const c3 = getDom("demo3-content3");
    c1.addEventListener(
      "click",
      () ={
        console.log(1);
      },
      BUBBLING
    );

    c3.addEventListener(
      "click",
      () ={
        console.log(3);
      },
      BUBBLING
    );
  }
  render() {
    return (
      <div class>
        <h3
          onClick={() ={
            this.setState({ show: !this.state.show });
          }}
        >
          demo3 混合使用
        </h3>

        <div class>
          1
          <div
            class
            onClick={() ={
              console.log(2);
            }}
          >
            2<div class>3</div>
          </div>
        </div>
        {this.state.show && (
          <p>
            3-1-2-document-window
            <br />
          </p>
        )}
      </div>
    );
  }
}

輸出結果:

黃色的數字 2 所在區域是 react 事件,1 和 3 是原生事件

到這裏的疑惑是,爲什麼 1 比 2 先打印出來?

簡單的來說,其實是 React(v16) 所有事件最後都是代理到 document 上面,通過冒泡機制先輸出 3 和 1,然後觸發 document 上的事件 打印 2。document 上掛載的兩個事件,第一個是我們在 <App /> 組件裏註冊的,第二個是 React 掛載的,如下圖所示。

4、原生事件阻止冒泡

import { Component } from "react";
import "./index.css";
const getDom = (className) ={
  return document.querySelector(`.${className}`);
};
// 冒泡/捕獲
const BUBBLING = false;
export default class Demo extends Component {
  state = {};
  componentDidMount() {
    const c1 = getDom("demo5-content1");
    const c2 = getDom("demo5-content2");
    const c3 = getDom("demo5-content3");
    c1.addEventListener(
      "click",
      () ={
        console.log(1);
      },
      BUBBLING
    );

    c2.addEventListener(
      "click",
      (e) ={
        console.log(2, "阻止原生事件冒泡");
        // 阻止冒泡或捕獲
        e.stopPropagation();
      },
      BUBBLING
    );

    c3.addEventListener(
      "click",
      () ={
        console.log(3);
      },
      BUBBLING
    );
  }
  
  render() {
    return (
      <div class>
        <h3
          onClick={() ={
            this.setState({ show: !this.state.show });
          }}
        >
          demo4 原生阻止冒泡
        </h3>

        <div class>
          1
          <div class>
            2<div class>3</div>
          </div>
        </div>
        {this.state.show && (
          <p>
            3-2
            <br />
            e.stopPropagation();
          </p>
        )}
      </div>
    );
  }
}

這裏也不難理解,紅色虛線爲阻止了冒泡

5、合成事件阻止冒泡

import { Component } from "react";
import "./index.css";
export default class Demo extends Component {
  state = {};
  render() {
    return (
      <div class>
        <h3
          onClick={() ={
            this.setState({ show: !this.state.show });
          }}
        >
          demo5 合成阻止冒泡
        </h3>
        <div
          class
          onClick={() ={
            console.log(1);
          }}
        >
          1
          <div
            class
            onClick={(e) ={
              e.stopPropagation();
              console.log(2);
            }}
          >
            2
            <div
              class
              onClick={() ={
                console.log(3);
              }}
            >
              3
            </div>
          </div>
        </div>
        {this.state.show && (
          <p>
            3-2-document
            <br />
            e.stopPropagation();
          </p>
        )}
      </div>
    );
  }
}

這裏可以看到,的確是阻止了 React 事件的冒泡,只輸出了 3 和 2。但是也有兩個問題。

我們知道,上面調用的e.stopPropagation(),其實是 React 事件本身處理過的。原生的事件需要通過 nativeEvent 獲取。來簡單看一下下面的代碼,React 是如何做的:

stopPropagation: function () {
  // 原生事件
  var event = this.nativeEvent;

  if (!event) {
    return;
  }

  if (event.stopPropagation) {
    // 原生事件阻止冒泡
    event.stopPropagation();
  } else if (typeof event.cancelBubble !== 'unknown') {
    // The ChangeEventPlugin registers a "propertychange" event for
    // IE. This event does not support bubbling or cancelling, and
    // any references to cancelBubble throw "Member not found".  A
    // typeof check of "unknown" circumvents this issue (and is also
    // IE specific).
    event.cancelBubble = true;
  }
  // 這裏應該是阻止 React合成事件冒泡的地方(待求證)
  this.isPropagationStopped = functionThatReturnsTrue;
},

當調用 React 事件本身的e.stopPropagation()的時候,React 調用了一次原生事件上的stopPropagation(),從而阻止了 window 上的事件。

針對第二個問題,來看下面的 Demo

6、合成事件與原生事件結合阻止冒泡

import { Component } from "react";
import "./index.css";
export default class Demo extends Component {
  state = {};
  render() {
    return (
      <div class>
        <h3
          onClick={() ={
            this.setState({ show: !this.state.show });
          }}
        >
          demo6 合成阻止冒泡
        </h3>
        <div
          class
          onClick={() ={
            console.log(1);
          }}
        >
          1
          <div
            class
            onClick={(e) ={
              console.log(2);
              e.nativeEvent.stopImmediatePropagation();
              e.stopPropagation();
            }}
          >
            2
            <div
              class
              onClick={() ={
                console.log(3);
              }}
            >
              3
            </div>
          </div>
        </div>
        {this.state.show && (
          <p>
            3-2
            <br />
            e.nativeEvent.stopImmediatePropagation()
            <br />
            e.stopPropagation()
            <br />
          </p>
        )}
      </div>
    );
  }
}

通過這種方法可以在原生事件和合成事件混合使用的時候,阻止事件的冒泡、捕獲。

主要注意這個方法stopImmediatePropagation(),MDN:

Event 接口的 stopImmediatePropagation() 方法阻止監聽同一事件的其他事件監聽器被調用。

如果多個事件監聽器被附加到相同元素的相同事件類型上,當此事件觸發時,它們會按其被添加的順序被調用。如果在其中一個事件監聽器中執行 stopImmediatePropagation() ,那麼剩下的事件監聽器都不會被調用。

調用 e.nativeEvent.stopImmediatePropagation(),阻止 document 上註冊的其它原生事件。React 可能是基於設計方面考慮,沒有實現這個方法。

總結

上面所示例的 demo 都是基於 React v16 的,v17 之後 React 將不再向 document 附加事件處理器。而會將事件處理器附加到渲染 React 樹的根 DOM 容器中:

const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

React 不建議將原生事件和合成事件一起使用,在開發過程中還是要儘量避免混合使用。瞭解了基本原理,靈活運用可以幫助我們有效的解決或者避免這方面的問題。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/8N2bcTmmLSJm0TiE-rYuyg