探索 React 合成事件

React 是一個 Facebook 開源的,用於構建用戶界面的 JavaScript 庫。

React 目的在於解決:構建隨着時間數據不斷變化的大規模應用程序。
其中 React 合成事件是較爲重要的知識點,閱讀完本文,你將收穫:javascript

  1. 合成事件的概念和做用;
  2. 合成事件與原生事件的 3 個區別;
  3. 合成事件與原生事件的執行順序;
  4. 合成事件的事件池;
  5. 合成事件 4 個常見問題。

接下來和我一塊兒開始學習吧~html

1、概念介紹

React 合成事件(SyntheticEvent)是 React 模擬原生 DOM 事件全部能力的一個事件對象,即瀏覽器原生事件的跨瀏覽器包裝器。它根據 W3C 規範 來定義合成事件,兼容全部瀏覽器,擁有與瀏覽器原生事件相同的接口。
看個簡單示例:java

const button = <button onClick={handleClick}>Leo 按鈕</button>

在 React 中,全部事件都是合成的,不是原生 DOM 事件,但能夠經過 e.nativeEvent 屬性獲取 DOM 事件。react

const handleClick = (e) => console.log(e.nativeEvent);;
const button = <button onClick={handleClick}>Leo 按鈕</button>

學習一個新知識的時候,必定要知道爲何會出現這個技術。
那麼 React 爲何使用合成事件?其主要有三個目的:git

  1. 進行瀏覽器兼容,實現更好的跨平臺

React 採用的是頂層事件代理機制,可以保證冒泡一致性,能夠跨瀏覽器執行。React 提供的合成事件用來抹平不一樣瀏覽器事件對象之間的差別,將不一樣平臺事件模擬合成事件。github

  1. 避免垃圾回收

事件對象可能會被頻繁建立和回收,所以 React 引入事件池,在事件池中獲取或釋放事件對象。即 React 事件對象不會被釋放掉,而是存放進一個數組中,當事件觸發,就從這個數組中彈出,避免頻繁地去建立和銷燬(垃圾回收)typescript

  1. 方便事件統一管理和事務機制
本文不介紹源碼啦,對具體實現的源碼有興趣的朋友能夠查閱: 《React SyntheticEvent》

2、原生事件回顧

在開始介紹 React 合成事件以前,咱們先簡單回顧 JavaScript 原生事件中幾個重要知識點:
Native-Event.png數組

1. 事件捕獲

當某個元素觸發某個事件(如 onclick ),頂層對象 document 就會發出一個事件流,隨着 DOM 樹的節點向目標元素節點流去,直到到達事件真正發生的目標元素。在這個過程當中,事件相應的監聽函數是不會被觸發的。瀏覽器

2. 事件目標

當到達目標元素以後,執行目標元素該事件相應的處理函數。若是沒有綁定監聽函數,那就不執行。性能優化

3. 事件冒泡

從目標元素開始,往頂層元素傳播。途中若是有節點綁定了相應的事件處理函數,這些函數都會被觸發一次。若是想阻止事件起泡,可使用 e.stopPropagation() 或者 e.cancelBubble=true(IE)來阻止事件的冒泡傳播。

4. 事件委託/事件代理

簡單理解就是將一個響應事件委託到另外一個元素
當子節點被點擊時,click 事件向上冒泡,父節點捕獲到事件後,咱們判斷是否爲所需的節點,而後進行處理。其優勢在於減小內存消耗和動態綁定事件

2、合成事件與原生事件區別

React 事件與原生事件很類似,但不徹底相同。這裏列舉幾個常見區別:

1. 事件名稱命名方式不一樣

原生事件命名爲純小寫(onclick, onblur),而 React 事件命名採用小駝峯式(camelCase),如 onClick 等:

// 原生事件綁定方式
<button onclick="handleClick()">Leo 按鈕命名</button>
      
// React 合成事件綁定方式
const button = <button onClick={handleClick}>Leo 按鈕命名</button>

2. 事件處理函數寫法不一樣

原生事件中事件處理函數爲字符串,在 React JSX 語法中,傳入一個函數做爲事件處理函數。

// 原生事件 事件處理函數寫法
<button onclick="handleClick()">Leo 按鈕命名</button>
      
// React 合成事件 事件處理函數寫法
const button = <button onClick={handleClick}>Leo 按鈕命名</button>

3. 阻止默認行爲方式不一樣

在原生事件中,能夠經過返回 false 方式來阻止默認行爲,可是在 React 中,須要顯式使用 preventDefault() 方法來阻止。
這裏以阻止 <a> 標籤默認打開新頁面爲例,介紹兩種事件區別:

// 原生事件阻止默認行爲方式
<a href="https://www.pingan8787.com" 
  onclick="console.log('Leo 阻止原生事件~'); return false"
>
  Leo 阻止原生事件
</a>

// React 事件阻止默認行爲方式
const handleClick = e => {
  e.preventDefault();
  console.log('Leo 阻止原生事件~');
}
const clickElement = <a href="https://www.pingan8787.com" onClick={handleClick}>
  Leo 阻止原生事件
</a>

4. 小結

小結前面幾點區別:

原生事件 React 事件
事件名稱命名方式 名稱所有小寫<br/>(onclick, onblur) 名稱採用小駝峯<br/>(onClick, onBlur)
事件處理函數語法 字符串 函數
阻止默認行爲方式 事件返回 false 使用 e.preventDefault() 方法

Native-Event-VS-Synthetic-Event.png

3、React 事件與原生事件執行順序

在 React 中,「合成事件」會以事件委託(Event Delegation)方式綁定在組件最上層,並在組件卸載(unmount)階段自動銷燬綁定的事件。這裏咱們手寫一個簡單示例來觀察 React 事件和原生事件的執行順序:

class App extends React.Component<any, any> {
  parentRef: any;
  childRef: any;
  constructor(props: any) {
    super(props);
    this.parentRef = React.createRef();
    this.childRef = React.createRef();
  }
  componentDidMount() {
    console.log("React componentDidMount!");
    this.parentRef.current?.addEventListener("click", () => {
      console.log("原生事件:父元素 DOM 事件監聽!");
    });
    this.childRef.current?.addEventListener("click", () => {
      console.log("原生事件:子元素 DOM 事件監聽!");
    });
    document.addEventListener("click", (e) => {
      console.log("原生事件:document DOM 事件監聽!");
    });
  }
  parentClickFun = () => {
    console.log("React 事件:父元素事件監聽!");
  };
  childClickFun = () => {
    console.log("React 事件:子元素事件監聽!");
  };
  render() {
    return (
      <div ref={this.parentRef} onClick={this.parentClickFun}>
        <div ref={this.childRef} onClick={this.childClickFun}>
          分析事件執行順序
        </div>
      </div>
    );
  }
}
export default App;

觸發事件後,能夠看到控制檯輸出:

原生事件:子元素 DOM 事件監聽! 
原生事件:父元素 DOM 事件監聽! 
React 事件:子元素事件監聽! 
React 事件:父元素事件監聽! 
原生事件:document DOM 事件監聽!

經過上面流程,咱們能夠理解:

  • React 全部事件都掛載在 document 對象上;
  • 當真實 DOM 元素觸發事件,會冒泡到 document 對象後,再處理 React 事件;
  • 因此會先執行原生事件,而後處理 React 事件;
  • 最後真正執行 document 上掛載的事件。

Native-Event-And-Synthetic-Event.png

4、合成事件的事件池**

1. 事件池介紹

合成事件對象池,是 React 事件系統提供的一種性能優化方式合成事件對象在事件池統一管理不一樣類型的合成事件具備不一樣的事件池

  • 當事件池未滿時,React 建立新的事件對象,派發給組件。
  • 當事件池裝滿時,React 從事件池中複用事件對象,派發給組件。

關於「事件池是如何工做」的問題,能夠看看下面圖片:

Synthetic-Event-Loop.png

(圖片來自:ReactDeveloper https://juejin.cn/post/6844903862285893639

2. 事件池分析(React 16 版本)

React 事件池僅支持在 React 16 及更早版本中,在 React 17 已經不使用事件池
下面以 React 16 版本爲例:

function handleChange(e) {
  console.log("原始數據:", e.target)
  setTimeout(() => {
    console.log("定時任務 e.target:", e.target); // null
    console.log("定時任務:e:", e); 
  }, 100);
}
function App() {
  return (
    <div className="App">
      <button onClick={handleChange}>測試事件池</button>
    </div>
  );
}

export default App;

能夠看到輸出:
Synthetic-Event-React16.png

在 React 16 及以前的版本,合成事件對象的事件處理函數所有被調用以後,全部屬性都會被置爲 null 。這時,若是咱們須要在事件處理函數運行以後獲取事件對象的屬性,可使用 React 提供的 e.persist() 方法,保留全部屬性:

// 只修改 handleChange 方法,其餘不變
function handleChange(e) {
  // 只增長 persist() 執行
  e.persist();
  
  console.log("原始數據:", e.target)
  setTimeout(() => {
    console.log("定時任務 e.target:", e.target); // null
    console.log("定時任務:e:", e); 
  }, 100);
}

再看下結果:

Synthetic-Event-React17.png

3. 事件池分析(React 17 版本)

因爲 Web 端的 React 17 不使用事件池,全部不會存在上述「全部屬性都會被置爲 null」的問題。

5、常見問題

1. React 事件中 this 指向問題

在 React 中,JSX 回調函數中的 this 常常會出問題,在 Class 中方法不會默認綁定 this,就會出現下面狀況, this.funName 值爲 undefined :

class App extends React.Component<any, any> {
  childClickFun = () => {
    console.log("React 事件");
  };
  clickFun() {
    console.log("React this 指向問題", this.childClickFun); // undefined
  }
  render() {
    return (
        <div onClick={this.clickFun}>React this 指向問題</div>
    );
  }
}
export default App;

咱們有 2 種方式解決這個問題:

  1. 使用 bind 方法綁定 this :
class App extends React.Component<any, any> {
  constructor(props: any) {
    super(props);
    this.clickFun = this.clickFun.bind(this);
  }
  
  // 省略其餘代碼
}
export default App;
  1. 將須要使用 this 的方法改寫爲使用箭頭函數定義:
class App extends React.Component<any, any> {
  clickFun = () => {
    console.log("React this 指向問題", this.childClickFun); // undefined
  }
  
  // 省略其餘代碼
}
export default App;

或者在回調函數中使用箭頭函數

class App extends React.Component<any, any> {
  // 省略其餘代碼
  clickFun() {
    console.log("React this 指向問題", this.childClickFun); // undefined
  }
  render() {
    return (
        <div onClick={() => this.clickFun()}>React this 指向問題</div>
    );
  }
}
export default App;

2. 向事件傳遞參數問題

常常在遍歷列表時,須要向事件傳遞額外參數,如 id 等,來指定須要操做的數據,在 React 中,可使用 2 種方式向事件傳參:

const List = [1,2,3,4];
class App extends React.Component<any, any> {
  // 省略其餘代碼
  clickFun (id) {console.log('當前點擊:', id)}
  render() {
    return (
        <div>
            <h1>第一種:經過 bind 綁定 this 傳參</h1>
            {
              List.map(item => <div onClick={this.clickFun.bind(this, item)}>按鈕:{item}</div>)
          }
            <h1>第二種:經過箭頭函數綁定 this 傳參</h1>
            {
              List.map(item => <div onClick={() => this.clickFun(item)}>按鈕:{item}</div>)
          }
        </div>
    );
  }
}
export default App;

這兩種方式是等價的:

  • 第一種經過 Function.prototype.bind 實現;
  • 第二種經過箭頭函數實現。

3. 合成事件阻止冒泡

官網文檔描述了:

從 v0.14 開始,事件處理器返回 false 時,再也不阻止事件傳遞。你能夠酌情手動調用 e.stopPropagation() 或 e.preventDefault() 做爲替代方案。

也就是說,在 React 合成事件中,須要阻止冒泡時,可使用 e.stopPropagation()e.preventDefault() 方法來解決,另外還可使用 e.nativeEvent.stopImmediatePropagation() 方法解決。

3.1 e.stopPropagation

對於開發者來講,更但願使用 e.stopPropagation() 方法來阻止當前 DOM 事件冒泡,但事實上,從前兩節介紹的執行順序可知,e.stopPropagation() 只能阻止合成事件間冒泡,即下層的合成事件,不會冒泡到上層的合成事件。事件自己還都是在 document 上執行。因此最多隻能阻止 document 事件不能再冒泡到 window 上。

class App extends React.Component<any, any> {
  parentRef: any;
  childRef: any;
  constructor(props: any) {
    super(props);
    this.parentRef = React.createRef();
  }
  componentDidMount() {
    this.parentRef.current?.addEventListener("click", () => {
      console.log("阻止原生事件冒泡~");
    });
    document.addEventListener("click", (e) => {
      console.log("原生事件:document DOM 事件監聽!");
    });
  }
  parentClickFun = (e: any) => {
    e.stopPropagation();
    console.log("阻止合成事件冒泡~");
  };
  render() {
    return (
      <div ref={this.parentRef} onClick={this.parentClickFun}>
        點擊測試「合成事件和原生事件是否能夠混用」
      </div>
    );
  }
}
export default App;

輸出結果:

阻止原生事件冒泡~ 
阻止合成事件冒泡~

3.2 e.nativeEvent.stopImmediatePropagation

該方法能夠阻止監聽同一事件的其餘事件監聽器被調用
在 React 中,一個組件只能綁定一個同類型的事件監聽器,當重複定義時,後面的監聽器會覆蓋以前的。
事實上 nativeEvent 的 stopImmediatePropagation只能阻止綁定在 document 上的事件監聽器。而合成事件上的 e.nativeEvent.stopImmediatePropagation()阻止合成事件不會冒泡到 document 上

舉一個實際案例:實現點擊空白處關閉菜單的功能:
當菜單打開時,在 document 上動態註冊事件,用來關閉菜單。

  • 點擊菜單內部,因爲不冒泡,會正常執行菜單點擊。
  • 點擊菜單外部,執行document上事件,關閉菜單。

在菜單關閉的一刻,在 document 上移除該事件,這樣就不會重複執行該事件,浪費性能,也能夠在 window 上註冊事件,這樣能夠避開 document。
**

4. 合成事件和原生事件是否能夠混用

合成事件和原生事件最好不要混用
原生事件中若是執行了stopPropagation方法,則會致使其餘React事件失效。由於全部元素的事件將沒法冒泡到document上。
經過前面介紹的二者事件執行順序來看,全部的 React 事件都將沒法被註冊。經過代碼一塊兒看看:

class App extends React.Component<any, any> {
  parentRef: any;
  childRef: any;
  constructor(props: any) {
    super(props);
    this.parentRef = React.createRef();
  }
  componentDidMount() {
    this.parentRef.current?.addEventListener("click", (e: any) => {
        e.stopPropagation();
      console.log("阻止原生事件冒泡~");
    });
    document.addEventListener("click", (e) => {
      console.log("原生事件:document DOM 事件監聽!");
    });
  }
  parentClickFun = (e: any) => {
    console.log("阻止合成事件冒泡~");
  };
  render() {
    return (
      <div ref={this.parentRef} onClick={this.parentClickFun}>
        點擊測試「合成事件和原生事件是否能夠混用」
      </div>
    );
  }
}
export default App;

輸出結果:

阻止原生事件冒泡~

好了,本文就寫到這裏,建議你們能夠再回去看下官方文檔《合成事件》《事件處理》章節理解,有興趣的朋友也能夠閱讀源碼《React SyntheticEvent.js》

總結

最後在回顧下本文學習目標:

  1. 合成事件的概念和做用;
  2. 合成事件與原生事件的 3 個區別;
  3. 合成事件與原生事件的執行順序;
  4. 合成事件的事件池;
  5. 合成事件 4 個常見問題。

你是否都清楚了?歡迎一塊兒討論學習。

參考文章

1.《事件處理與合成事件(react)》
2.官方文檔《合成事件》《事件處理》
3.《React合成事件和DOM原生事件混用須知》
4.《React 合成事件系統之事件池》

相關文章
相關標籤/搜索