React 事件機(jī)制

1、原生Dom的事件流

image.png

如上圖所示:
在JavaScript中,事件的觸發(fā)實(shí)質(zhì)上是要經(jīng)過(guò)三個(gè)階段:事件捕獲、目標(biāo)對(duì)象本身的事件處理和事件冒泡。

  • 事件捕獲 (由父級(jí)元素將事件一直傳遞到事件發(fā)生的元素)
    當(dāng)某個(gè)事件觸發(fā)時(shí),文檔根節(jié)點(diǎn)最先接受到事件,然后根據(jù)DOM樹結(jié)構(gòu)向具體綁定事件的元素傳遞。該階段為父元素截獲事件提供了機(jī)會(huì)。
    事件傳遞路徑為:
    window —> document —> body —> div—> text

  • 目標(biāo)階段
    具體元素已經(jīng)捕獲事件后,執(zhí)行目標(biāo)事件本身的處理事件。

  • 事件冒泡
    根據(jù)DOM樹結(jié)構(gòu)由具體觸發(fā)事件的元素向根節(jié)點(diǎn)傳遞。正因?yàn)槭录贒OM的傳遞經(jīng)歷這樣一個(gè)過(guò)程,從而為事件委托提供了可能。
    事件傳遞路徑:
    text—> div —> body —> document —> window

2、事件委托(事件代理)

事件委托的實(shí)質(zhì)就是將子元素事件的處理委托給父級(jí)元素處理。把事件監(jiān)聽(tīng)器添加到它們的父元素上。事件監(jiān)聽(tīng)器會(huì)分析從子元素冒泡上來(lái)的事件,找到它是哪個(gè)子元素的事件。

舉個(gè)栗子:如果我們有一個(gè)列表,列表之中有大量的列表項(xiàng),我們需要在點(diǎn)擊列表項(xiàng)的時(shí)候響應(yīng)一個(gè)事件

<body>
    <div class="lists">
        <div class="list-item">content1</div>
        <div class="list-item">content2</div>
        <div class="list-item">content3</div>
        <div class="list-item">content4</div>
    </div>

    <script>
        //1. 事件代理
        // var listParentNode = document.querySelector('.lists');
        // listParentNode.addEventListener('click', function(e){
        //     console.log(e.target);
        // })

        //新增一個(gè)子元素
        // var newItem = document.createElement('div')
        // newItem.innerHTML = 'newConent';
        // newItem.setAttribute('class', 'list-item');
        // listParentNode.appendChild(newItem)

       
        //2. 給每個(gè)列表項(xiàng)一一綁定一個(gè)函數(shù)
        var nodeLists = document.querySelectorAll('.list-item');
        for(var i = 0; i < nodeLists.length; i++) {
            nodeLists[i].addEventListener('click', function(e){
                console.log(e.target)
            })
        }
        //新增一個(gè)子元素
        var listParentNode = document.querySelector('.lists');
        var newItem = document.createElement('div')
        newItem.innerHTML = 'newConent';
        newItem.setAttribute('class', 'list-item');
        listParentNode.appendChild(newItem)

</body>

事件委托優(yōu)點(diǎn)

  1. 減少內(nèi)存的消耗
    上面兩種方式, 如果給每個(gè)列表項(xiàng)一一都綁定一個(gè)函數(shù),那對(duì)于內(nèi)存消耗是非常大的,效率上需要消耗很多性能;所以比較好的方法是 點(diǎn)擊事件綁定到他的父層,然后在執(zhí)行事件的時(shí)候再去匹配判斷目標(biāo)元素(這樣用事件代理的方式綁定事件,新添加的子元素仍然可以執(zhí)行事件回調(diào)函數(shù))。

  2. 動(dòng)態(tài)綁定事件
    比如上述的例子中列表項(xiàng)就幾個(gè),我們給每個(gè)列表項(xiàng)都綁定了事件;
    在很多時(shí)候,我們需要通過(guò) AJAX 或者用戶操作動(dòng)態(tài)的增加或者去除列表項(xiàng)元素,那么在每一次改變的時(shí)候都需要重新給新增的元素綁定事件,給即將刪去的元素解綁事件;
    如果用了事件委托就沒(méi)有這種麻煩了,因?yàn)槭录墙壎ㄔ诟笇拥?,和目?biāo)元素的增減是沒(méi)有關(guān)系的,執(zhí)行到目標(biāo)元素是在真正響應(yīng)執(zhí)行事件函數(shù)的過(guò)程中去匹配的;
    所以使用事件在動(dòng)態(tài)綁定事件的情況下是可以減少很多重復(fù)工作的。

不適用的情況: 比如 focus、blur 之類的事件本身沒(méi)有事件冒泡機(jī)制,所以無(wú)法委托; mousemove、mouseout這樣的事件,雖然有事件冒泡,但是只能不斷通過(guò)位置去計(jì)算定位,對(duì)性能消耗高,因此也是不適合于事件委托。

3. React事件機(jī)制

image.png

React事件介紹 中介紹了合成事件對(duì)象以及為什么提供合成事件對(duì)象,主要原因是因?yàn)?React 想實(shí)現(xiàn)一個(gè)全瀏覽器的框架, 為了實(shí)現(xiàn)這種目標(biāo)就需要提供全瀏覽器一致性的事件系統(tǒng),以此抹平不同瀏覽器的差異。

合成事件對(duì)象很有意思,一開(kāi)始聽(tīng)名字會(huì)覺(jué)得很奇怪,看到英文名更奇怪 SyntheticEvent, 實(shí)際上合成事件的意思就是使用原生事件合成一個(gè) React 事件, 例如使用原生click事件合成了onClick事件,使用原生mouseout事件合成了onMouseLeave事件,原生事件和合成事件類型大部分都是一一對(duì)應(yīng),只有涉及到兼容性問(wèn)題時(shí)我們才需要使用不對(duì)應(yīng)的事件合成。

合成事件是瀏覽器的原生事件的跨瀏覽器包裝器。除兼容所有瀏覽器外,它還擁有和瀏覽器原生事件相同的接口,包括 stopPropagation()preventDefault()。

當(dāng)我們?cè)诮M件上設(shè)置事件處理器時(shí),React并不會(huì)在該DOM元素上直接綁定事件處理器. React內(nèi)部自定義了一套事件系統(tǒng),在這個(gè)系統(tǒng)上統(tǒng)一進(jìn)行事件訂閱和分發(fā)。

具體來(lái)講,React利用事件委托機(jī)制在Document上統(tǒng)一監(jiān)聽(tīng)DOM事件,再根據(jù)觸發(fā)的target將事件分發(fā)到具體的組件實(shí)例。另外上面e是一個(gè)合成事件對(duì)象(SyntheticEvent), 而不是原始的DOM事件對(duì)象。

React事件系統(tǒng)實(shí)現(xiàn)可以分為兩個(gè)階段:事件注冊(cè)、事件觸發(fā)

  • 事件注冊(cè)
    React 的事件注冊(cè)過(guò)程主要做了兩件事:document 上注冊(cè)、存儲(chǔ)事件回調(diào)
    document 上注冊(cè)
    React在組件加載(mount)和更新(update)時(shí), 根據(jù)傳入組件內(nèi)的聲明的事件類型的屬性(onClick、onChange 等),在 document 上注冊(cè)事件(document.addEventListener('click', dispatchEvent)) 并指定統(tǒng)一的回調(diào)函數(shù) dispatchEvent(不處理具體的事件,僅對(duì)事件進(jìn)行分發(fā))
    存儲(chǔ)事件回調(diào)
    React 為了在觸發(fā)事件時(shí)可以查找到對(duì)應(yīng)的回調(diào)去執(zhí)行,會(huì)把組件內(nèi)的所有事件統(tǒng)一地存放到一個(gè)對(duì)象中(listenerBank)。而存儲(chǔ)方式如上圖,首先會(huì)根據(jù)事件類型分類存儲(chǔ),例如 click 事件相關(guān)的統(tǒng)一存儲(chǔ)在一個(gè)對(duì)象中,回調(diào)函數(shù)的存儲(chǔ)采用鍵值對(duì)(key/value)的方式存儲(chǔ)在對(duì)象中,key 是組件的唯一標(biāo)識(shí) (_rootNodeID),value 對(duì)應(yīng)的就是事件的回調(diào)函數(shù)。例如
{
    click: {
        key1: fn, //key 是組件的唯一標(biāo)識(shí) (_rootNodeID)
        key2: fn
    },
    change: {
        key3: fn,
        key4: fn
    }
}

ReactBrowserEventEmitter作為事件注冊(cè)入口,擔(dān)負(fù)著事件注冊(cè)和事件觸發(fā)。注冊(cè)事件的回調(diào)函數(shù)由EventPluginHub來(lái)統(tǒng)一管理,根據(jù)事件的類型(type)和組件標(biāo)識(shí)(_rootNodeID)為key唯一標(biāo)識(shí)事件并進(jìn)行存儲(chǔ)。

  • 事件觸發(fā)


    事件觸發(fā)流程.png

其大致流程如下:

  1. 觸發(fā)事件,開(kāi)始 DOM 事件流,先后經(jīng)過(guò)三個(gè)階段:事件捕獲階段、處于目標(biāo)階段和事件冒泡階段
  2. 當(dāng)事件冒泡到 document 時(shí),觸發(fā)統(tǒng)一的事件分發(fā)函數(shù) ReactEventListener.dispatchEvent
  3. dispatchEvent根據(jù)原生事件對(duì)象(nativeEvent)找到當(dāng)前節(jié)點(diǎn)(即事件觸發(fā)節(jié)點(diǎn))對(duì)應(yīng)的 ReactDOMComponent 對(duì)象
  4. 事件的合成
    (1) 根據(jù)當(dāng)前事件類型生成對(duì)應(yīng)的合成對(duì)象
    (2) 封裝原生事件對(duì)象和冒泡機(jī)制
    (3) 查找當(dāng)前元素以及它所有父級(jí)
    (4)在 listenerBank 中查找事件回調(diào)函數(shù)并合成到 events 中
  5. 批量執(zhí)行合成事件(events)內(nèi)的回調(diào)函數(shù),
  6. 如果沒(méi)有阻止冒泡,會(huì)將繼續(xù)進(jìn)行 DOM 事件流的冒泡(從 document 到 window),否則結(jié)束事件觸發(fā)

簡(jiǎn)單點(diǎn)的解釋為:
React不會(huì)將事件處理函數(shù)直接綁定到真實(shí)的節(jié)點(diǎn)上,而是把所有的事件綁定到結(jié)構(gòu)的最外層,使用一個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器。這個(gè)監(jiān)聽(tīng)器維持了一個(gè)映射,保存所有組件內(nèi)部的事件監(jiān)聽(tīng)和處理函數(shù)。當(dāng)事件發(fā)生時(shí),首先被這個(gè)統(tǒng)一的事件監(jiān)聽(tīng)器處理,然后在映射里找到真正的事件處理函數(shù)并調(diào)用。

需要注意的是

  1. React合成事件的冒泡并不是真的冒泡,而是節(jié)點(diǎn)的遍歷。
  2. 并不是所有事件都會(huì)委托到document上,前面提到幾乎所有的事件代理(delegate)到document,幾乎說(shuō)明存在例外的情況。例如對(duì)于audio、video標(biāo)簽,存在一些媒體事件(例如onplay、onpause),而這些事件是document不具有的,那么只能在這些標(biāo)簽上進(jìn)行事件綁定,綁定一個(gè)入口分發(fā)函數(shù)(dispatchEvent)。

4. React事件和原生事件有什么區(qū)別

關(guān)于合成事件
合成事件官方文檔:https://react.html.cn/docs/events.html

合成事件作用

  1. 對(duì)原生事件封裝
    在事件回調(diào)方法,方法中的參數(shù) e,其實(shí)不是原生事件對(duì)象e而是react包裝過(guò)的對(duì)象,同時(shí)原生事件對(duì)象被放在了屬性 e.nativeEvent內(nèi)。
    官網(wǎng)中介紹到了 SyntheticEvent是react合成事件的基類,定義了合成事件的基礎(chǔ)公共屬性和方法。react會(huì)根據(jù)當(dāng)前的事件類型來(lái)使用不同的合成事件對(duì)象,比如鼠標(biāo)單機(jī)事件 - SyntheticMouseEvent,焦點(diǎn)事件-SyntheticFocusEvent等,但是都是繼承自SyntheticEvent。
  2. 對(duì)原生事件的升級(jí)和改造
    對(duì)于有些dom元素事件,我們進(jìn)行事件綁定之后,react并不是只處理你聲明的事件類型,還會(huì)額外的增加一些其他的事件,幫助我們提升交互的體驗(yàn)。
    最典型的例子就是input的onChange事件。
    image.png
  3. 瀏覽器事件的兼容處理
    react在給document注冊(cè)事件的時(shí)候也是對(duì)兼容性做了處理。下面這個(gè)代碼就是給document注冊(cè)事件,內(nèi)部其實(shí)也是做了對(duì) ie瀏覽器的兼容做了處理。


    image.png

React事件和原生事件主要區(qū)別有:

  1. React 組件上聲明的事件沒(méi)有綁定在 React 組件對(duì)應(yīng)的原生 DOM 節(jié)點(diǎn)上。
  2. React 利用事件委托機(jī)制,將幾乎所有事件的觸發(fā)代理(delegate)在 document 節(jié)點(diǎn)上,事件對(duì)象(event)是合成對(duì)象(SyntheticEvent),不是原生事件對(duì)象,但通過(guò) nativeEvent 屬性訪問(wèn)原生事件對(duì)象。
  3. 由于 React 的事件委托機(jī)制,React 組件對(duì)應(yīng)的原生 DOM 節(jié)點(diǎn)上的事件觸發(fā)時(shí)機(jī)總是在 React 組件上的事件之前。
  4. 原生事件阻止冒泡肯定會(huì)阻止合成事件的觸發(fā), 合成事件的阻止冒泡不會(huì)影響原生事件, 兩者最好不要混合使用,避免出現(xiàn)一些奇怪的問(wèn)題。

合成事件優(yōu)點(diǎn):

  1. 減少內(nèi)存消耗,提升性能,不需要注冊(cè)那么多的事件了,一種事件類型只在 document 上注冊(cè)一次
  2. 統(tǒng)一規(guī)范,解決兼容問(wèn)題,簡(jiǎn)化事件邏輯,事件處理程序接收到的是SyntheticEvent的實(shí)例。SyntheticEvent完全符合W3C的標(biāo)準(zhǔn),因此在事件層次上具有瀏覽器兼容性。
  3. React打算干預(yù)事件的分發(fā)。v16引入Fiber架構(gòu),React為了優(yōu)化用戶的交互體驗(yàn),會(huì)干預(yù)事件的分發(fā)。不同類型的事件有不同的優(yōu)先級(jí),比如高優(yōu)先級(jí)的事件可以中斷渲染,讓用戶代碼可以及時(shí)響應(yīng)用戶交互

站在巨人的肩膀上(參考資料)
dom事件
React事件機(jī)制和未來(lái) 這個(gè)文章寫得好!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容