React
有着獨特的事件機制-合成事件,React
的初學者確定碰到過這種問題,使用event.stopPropagation();
,卻仍是沒法禁止當前組件的事件冒泡,這就是React
的事件機制的緣由,它並不與DOM
事件相同。react
本文將從源碼的角度來解析,React
的事件系統究竟是如何實現的?git
DOM
事件流屬於比較基礎的知識點,本文不會詳細的再敘述,只列出須要一些關鍵點。W3C標準約定了一個事件的傳播過程要通過如下 3 個階段:github
經過DOM
事件流,咱們常常會用到一直常見的性能優化思路:事件委託。數組
React
事件系統也是基於事件委託這個特性實現的。瀏覽器
在React
中,除了一些不可冒泡的事件外,其它的事件都不會被綁定在具體的元素上,而是統一被綁定到document
上(17版本以後修改成綁定到React
的根DOM組件上),當事件在具體的DOM
節點上被觸發後,最終都會冒泡到document
上,React
根組件上所綁定的統一事件處理程序會將事件分發到具體的組件實例。性能優化
在分發事件以前,React
首先會對事件進行包裝,把原生DOM
事件包裝成合成事件。markdown
合成事件是React
自定義的事件對象,它在底層抹平了不一樣瀏覽器的差別,在上層面向開發者暴露統一的、穩定的、與DOM
原生事件相同的事件接口。app
雖然合成事件並非原生DOM
事件,但它保存了原生DOM
事件的引用。當你須要訪問原生DOM
事件對象時,能夠經過合成事件對象的e.nativeEvent
屬性獲取到原生DOM
事件。dom
事件的綁定是在組件的首次渲染鏈路的completeWork
方法中完成的。關於首次渲染的流程,能夠看我以前的文章異步
寫給本身看的React源碼解析(一):你的React代碼是怎麼渲染成DOM的?
completeWork
主要作了三件事情:
再finalizeInitialChildren
方法中,會遍歷節點的props
。當遍歷到事件相關的props
時,就會觸發事件的註冊鏈路。
本文基於16.13版本的React
源碼。注意:17版本的事件系統在源碼上的改動比較大,源碼鏈路上跟本文並不一致。
在ensureListeningTo
中,會獲取當前DOM
中的document
對象,而後經過調用legacyListenToEvent
,將統一的事件監聽函數註冊到document
上面。
在legacyListenToEvent
中,其實是經過調用legacyListenToTopLevelEvent
來處理事件和document
之間的關係的。 legacyListenToTopLevelEvent
直譯過來是「監聽頂層的事件」,這裏的「頂層」就能夠理解爲事件委託的最上層,也就是document
節點。
注意:在17版本中,流程中不會存在ensureListeningTo
和legacyListenToEvent
方法,React
會在finalizeInitialChildren
方法下的setInitialProperties
根據節點的tag
類型,傳入不一樣的參數並調用listenToNonDelegatedEvent
方法。在這個方法裏,會直接調用addTrappedEventListener
添加事件到React
的根組件DOM
元素上。
咱們接着來看,最終註冊到document
上的並非某一個DOM
節點上對應的具體回調邏輯,而是一個統一的事件分發函數listener
,它的本體是一個dispatchEvent
。
事件的觸發其實就是對於dispatchEvent
函數的調用。
咱們根據下面的這個demo來走流程
import React from 'react';
import { useState } from 'react'
function App() {
const [state, setState] = useState(0);
return (
<div className="App"> <div className="container" onClickCapture={() => console.log('捕獲通過 div')} onClick={() => console.log('冒泡通過 div')} > <p>{state}</p> <button onClick={() => { setState(state + 1) }}>點擊+1</button> </div> </div>
);
}
export default App;
複製代碼
這個demo的功能很簡單,每次點擊按鈕都會給state
加1。並給container
這個div上添加了兩個點擊事件,一個捕獲事件,一個冒泡事件。下圖是這個demo的fiber
樹結構。
收集的邏輯過程在traverseTwoPhase
函數
function traverseTwoPhase(inst, fn, arg) {
// 定義一個 path 數組
var path = [];
while (inst) {
// 將當前節點收集進 path 數組
path.push(inst);
// 向上收集 tag===HostComponent 的父節點
inst = getParent(inst);
}
var i;
// 從後往前,收集 path 數組中會參與捕獲過程的節點與對應回調
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
// 從前日後,收集 path 數組中會參與冒泡過程的節點與對應回調
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
複製代碼
traverseTwoPhase
函數作了如下三件事情。
traverseTwoPhase
會以觸發事件的目標節點爲起點,經過getParent
方法,不斷向上尋找tag===HostComponent
的父節點,並將這些節點按順序收集進path
數組中。tag===HostComponent
的節點是DOM
元素對應的的fiber
節點類型,也就是說只收集DOM元素對應的節點。
按照demo中的fiber
樹來講,最後收集到的節點爲div#container
、div.App
及button
節點。
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
複製代碼
path
數組是從子節點出發,向上收集得來的。因此,模擬事件的捕獲順序,須要從後往前遍歷path
數組。在遍歷的過程當中,fn
函數檢測每一個節點的事件回調,若該節點上對應當前事件的捕獲回調不爲空,那麼節點fiber
實例會被收集到合成事件的SyntheticEvent._dispatchInstances
中,事件回調則會被收集到合成事件的SyntheticEvent._dispatchListeners
屬性。
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
複製代碼
這裏功能跟上一步一致,區別只是從前日後來遍歷path
數組。
最後,咱們來看下SyntheticEvent
對象上的_dispatchInstances
和_dispatchListeners
。
咱們只要按順序調用執行回調函數,就可以模擬出DOM
事件流,也就是 「捕獲-目標-冒泡」這三個階段。
上文的源碼解析是基於16.13.x
版本的,17版本以後的事件系統,有了挺大的區別。
React
的根組件dom上onScroll
事件再也不冒泡onFocus
和onBlur
事件已在底層切換爲原生的focusin
和focusout
事件onClickCapture
如今使用的是實際瀏覽器中的捕獲監聽器(合成事件只會存在listenToNonDelegatedEvent
添加的冒泡事件)SyntheticEvent
再也不復用,在點擊事件中使用異步方法也將能夠獲取到點擊事件。不須要再使用e.persist()
方法若是本文對你有所幫助,請幫忙點個贊,感謝!