「React 深入」React 事件系統與原生事件系統究竟有何區別?

大家好,我是小杜杜,我們知道React自身提供了一套虛擬的事件系統,這套系統與原生系統到底有何區別?今天一起來詳細看看

作爲一個前端,我們會有很多的事件處理,所以學好React事件系統是非常有必要的

關於React事件系統將會分三個方面去講解,分別是:React 事件系統與原生事件系統、深入 React v16 事件系統、對比 Reactv16~v18 事件系統 三個模塊,有感興趣的可以關注下新的專欄:React 深入進階,一起進階學習~

在正式開始前,讓我們一起來看看以下幾個問題:

  1. React爲什麼要單獨整一套虛擬的事件系統,這樣做有什麼好處?

  2. React的事件系統,與原生的事件系統有什麼區別?

  3. 什麼是合成事件?

  4. React中是如何模擬冒泡和捕獲階段的?

  5. 所有的事件真的綁定在真實DOM上嗎?如果不是,那麼綁定在哪裏?

  6. React中的阻止冒泡與原生事件系統的阻止冒泡一樣嗎,爲什麼?

  7. ...

如果你能耐心的看完,相信一定能幫你更好的瞭解事件系統,先附上一張知識圖,供大家更好的觀看,還請各位小夥伴多多支持~

原生 DOM 事件

在講React的事件系統前,我們先複習一下原生DOM事件的概念,來幫助我們更好的理解

註冊事件

註冊事件:通過給元素添加點擊 (滾動等) 事件,稱作註冊事件,也叫綁定事件

註冊事件共有兩種方式,分別是:傳統註冊方式和監聽註冊方式

傳統註冊方式

傳統註冊方式:是以on開頭的方式,完成註冊方式

如:

// 第一種
    <button onclick="console.log(1)">點擊</button>

// 第二種
    <button id="btn">點擊</button>

    const btn = document.querySelector('#btn')
    btn.onclick = function () {}

需要注意的是:我們註冊的事件都具備唯一性,也就是同一個元素只能設置一個處理的函數,如果有多個,則會進行覆蓋

監聽註冊方式

監聽註冊方式:是以addEventListener方法來監聽元素事件

addEventListener方法並不支持IE 9以下的瀏覽器,當有需要的時候可以使用attachEvent方法,這個方法支持IE 10以下的瀏覽器,但此方法建並非標準

如:

   <button id="btn">點擊</button>
   
   const btn = document.querySelector('#btn')
   btn.addEventListener('click'() ={})

addEventListener 與傳統的方式不同,支持多次綁定事件,但要比傳統方式等級低

事件流

DOM事件流共分爲三個階段:事件捕獲目標事件冒泡三個階段

事件捕獲:由 DOM 最頂層節點開始,然後逐級向下傳播到最具體的元素接收的過程

事件冒泡:事件開始時由最具體的元素接收,然後逐級向上傳播到 DOM 最頂層節點的過程

特別注意:

擴展:阻止冒泡

這裏簡單介紹一下e.stopPropagation()e.stopImmediatePropagation()的區別,方便大家更好的理解,

舉個栗子🌰:

  <div id="id">
    <button id="btn">點擊</button>
  </div> 

  const div = document.querySelector('#id')
  const btn = document.querySelector('#btn')

  document.addEventListener('click'(e) ={
    console.log(1)
  })

  div.addEventListener('click'(e) ={
    console.log(2)
  })

  div.addEventListener('click'(e) ={
    console.log(3)
  })

  div.addEventListener('click'(e) ={
    console.log(4)
  })

  btn.addEventListener('click'() ={
    console.log(5)
  })

當我們點擊按鈕的時候,執行順序是:5 > 2 > 3 > 4 > 1,原因是執行了冒泡,會從最底層的btn開始執行,然後是div,最後纔是頂層document, 然後根據 Js 的執行順序,分別是 2、3 、4

那麼我們在console.log(3)上加入e.stopPropagation()看看,結果是什麼?

可以看到執行順序變成了:5 > 2 > 3 > 4

同樣的,我們換成e.stopImmediatePropagation()來看看結果:

此時結果變成了:5 > 2 > 3

結論:e.stopImmediatePropagation() 相當於e.stopPropagation()的增強版,不但可以阻止向上的冒泡,還能夠阻止同級的擴散

擴展:獲取事件流的階段

我們如果想知道觸發事件的元素屬於三個階段的哪個階段時,可以通過e.eventPhase來獲取

e.eventPhase爲 1 時,代表捕獲階段,爲 2 時,代表目標階段,爲 3 時,代表冒泡階段

事件委託

事件委託:也稱事件代理,在 JQ 中稱事件委派,也就是利用事件冒泡,將子級的事件委託給父級加載

也就是說,我們可以通過將監聽節點設置在父級上,然後利用冒泡來影響子集,如:

  <ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
  </ul> 
  
  const ul = document.querySelector('ul')
  const lis = ul.children

  ul.addEventListener('click'(e) ={
    for(let i = 0; i < lis.length; i++) {
      lis[i].style.background = ""
    }
    e.target.style.background = "red"
  })

我們點擊對應的節點,點擊的節點需要高亮,如果設置在子級上,就需要監聽所有子級節點,就非常麻煩,此時我們可以監聽父級,來實現效果

初探 React 事件系統

點擊事件究竟去了哪?

我們先來看看原生 DOM 事件,用傳統 / 監聽註冊方式下,事件綁定在何處?

可以看出,原生 DOM 事件就綁定在對應的button上,那麼 React 中,也是如此嗎?

我們發現在React中,button 這個元素並沒有綁定事件,並且對應的點擊事件中有buttondocument兩個事件

先來看看button事件,綁定的方法爲nonp

然而綁定的 nonp只是一個空函數,也就說,真正的事件綁定到了document

點擊事件究竟存儲到了哪?

之前在「React 深入」一文喫透虛擬 DOM 和 diff 算法中講過,我們編寫的jsx代碼首先會被babel轉化爲React.createElement,最終被轉化爲fiber對象

接下來我們逐步看看上述的代碼轉化後的樣子:

React.createElement形式:

fiber對象形式(可以選中當前元素,然後輸入console.dir($0)查看當前元素):

可以發現,事件最終保存在fiber中的memoizedProps 和 pendingProps

什麼是合成事件?

合成事件(SyntheticEvent):是React模擬原生 DOM 事件所有能力的一個事件對象,即瀏覽器原生事件的跨瀏覽器包裝器

簡單的來講,我們在上述的例子中,使用onClick點擊事件,而onClick並不是原生事件,而是由原生事件合成的React事件

爲了更好的理解,我們來看看inputonChang事件

export default function App(props) {
  return (
    <div>
      <input onChange={(e) => console.log(e)}></input>
    </div>
  );
}

此時在doncument上的事件爲

也就是說,我們綁定的onChange事件最終被處理成了很多事件的監聽器,如:blurchangefocusinput

爲什麼要單獨整一套呢?

React爲什麼要採取事件合成的模式呢?這樣做有什麼好處呢?

兼容性,跨平臺

我們知道有多種瀏覽器,每個瀏覽器的內核都不相同,而React通過頂層事件代理機制,保證冒泡的統一性,抹平不同瀏覽器事件對象之間的差異,將不同平臺的事件進行模擬成合成事件,使其能夠跨瀏覽器執行

將所有事件統一管理

在原生事件中,所有的事件都綁定在對應的Dom上,如果頁面複雜,綁定的事件會非常多,這樣就會造成一些不可控的問題

React將所有的事件都放在了document上,這樣就可以對事件進行統一管理,從而避免一些不必要的麻煩

避免垃圾回收

我們來看看React原生事件中的input都綁定onChange事件是什麼樣子?

可以看出,原生事件綁定onchange對應的就是change,而React會被處理爲很多的監聽器

在實際中,我們的事件會被頻繁的創建和回收,這樣會影響其性能,爲了解決這個問題,React引入事件池,通過事件池來獲取和釋放事件。

也就是說,所有的事件並不會被釋放,而是存入到一個數組中,如果這個事件觸發,則直接在這個數組中彈出即可,這樣就避免了頻繁創建和銷燬

淺談合成事件與原生事件

執行順序對比

我們先模擬下React中的合成事件和原生中的事件順序,如:

import React, {useEffect, useRef} from "react";

export default function App(props) {

  const ref = useRef(null)
  const ref1 = useRef(null)

  useEffect(() ={
    const div = document.querySelector("div")
    const button = document.querySelector("button")

    div.addEventListener("click"() => console.log("原生:div元素"))
    button.addEventListener("click"() => console.log("原生:button元素"))
    document.addEventListener("click"() => console.log("原生:document元素"))
  }[])

  return (
    <div onClick={() => console.log('React:div元素')}>
      <button
        onClick={() => console.log('React:按鈕元素')}
      >
        執行順序
      </button>
    </div>
  );
}

執行結果:

由上圖可看出,當DOM(button) 元素觸發後,會先執行原生事件,再處理React時間,最後真正執行document上掛載的事件

與原生事件有何不同?

事件名不同

接受的參數不同

事件源不同,阻止默認事件的方式不同

React中,我們的所有事件都可以說是虛擬的,並不是原生的事件

我們在React中拿到的事件源 (e) 也並非是真正的事件e,而是經過React 單獨處理的e

特別注意,原生事件和合成時間的e.preventDefault()並非是同一個函數,React 的事件源 e 是單獨創立的,所以兩者的方法也不相同,同時return false也在React中無效

擴展:對比 e.stopPropagation() 和 e.nativeEvent.stopImmediatePropagation

來擴展下阻止冒泡的方法:

爲了更好的說明,我們分別使用e.stopPropagation()e.nativeEvent.stopImmediatePropagation有什麼效果:

正常觸發:

e.stopPropagation()觸發:

e.nativeEvent.stopImmediatePropagation觸發:

從上圖可知:

擴展:冒泡和捕獲階段

React中,所有的綁定事件(如:onClickonChange)都是冒泡階段執行。

所有的捕獲階段統一加Capture,如onClickCaptureonChangeCapture

舉個小例子:

    <button
      onClick={() ={console.log('冒泡')}} 
      onClickCapture={() ={console.log("捕獲")}} >
        點擊
    </button>

是否可以混用?

我們通過對比原生事件和合成事件後,提出一個疑問,原生事件和合成事件是否可以一起使用?

先來舉個栗子🌰,一起看看

import React, {useEffect, useRef} from "react";

export default function App(props) {

  useEffect(() ={
    const button = document.querySelector("button")

    button.addEventListener("click"(e) ={
      e.stopPropagation();
      console.log("原生button阻止冒泡");
    })
    document.addEventListener("click"() ={
      console.log("原生document元素");
    });
  }[])

  return (
      <button
        onClick={() ={
          console.log('按鈕事件')
        }}
      >
        混用
      </button>
  );
}

結果:

可以發現只執行了原生事件,並沒有執行合成事件,這是因爲原生事件的執行順序在合成事件之前,所以導致合成事件沒有辦法進行註冊。

所以兩者建議不要進行混用,否則會跳過React的事件機制

End

參考

結語

本文通過對比React 事件系統原生事件系統,詳細的瞭解兩者的區別,實際上React上的事件都綁定在了document上,就連事件源也並非是原生中的事件源

那麼,React究竟如何綁定事件的,又是如何觸發事件的?爲什麼我們必須通過this去綁定對應的事件?又是如何處理批量更新的?... 都是一些我們值得探討的問題。

感興趣的可以關注下這個專欄,這個專欄會以進階爲目的,詳細講解React相關的原理、源碼、實戰,有感興趣的可以關注下,一起學習,一起進步~

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