深入分析React源碼中的合成事件
熱身準(zhǔn)備
明確幾個(gè)概念
在React@17.0.3版本中:
- 所有事件都是委托在
id = root的DOM元素中(網(wǎng)上很多說是在document中,17版本不是了); - 在應(yīng)用中所有節(jié)點(diǎn)的事件監(jiān)聽其實(shí)都是在
id = root的DOM元素中觸發(fā); React自身實(shí)現(xiàn)了一套事件冒泡捕獲機(jī)制;React實(shí)現(xiàn)了合成事件SyntheticEvent;React在17版本不再使用事件池了(網(wǎng)上很多說使用了對(duì)象池來管理合成事件對(duì)象的創(chuàng)建銷毀,那是16版本及之前);- 事件一旦在
id = root的DOM元素中委托,其實(shí)是一直在觸發(fā)的,只是沒有綁定對(duì)應(yīng)的回調(diào)函數(shù);

盜用一張官方圖,按官方解釋,之所以會(huì)將事件委托從document中移到id = root的DOM元素,是為了可以更加安全地進(jìn)行新舊版本 React 樹的嵌套。
感興趣的可以訪問:React中文網(wǎng)站 。
事件系統(tǒng)角色劃分
- 事件注冊(cè):
registerEvents; - 事件監(jiān)聽:
listenToAllSupportedEvents; - 事件合成:
SyntheticBaseEvent; - 事件派發(fā):
dispatchEvent;
事件注冊(cè)
事件注冊(cè)是自執(zhí)行的,也就是React自身進(jìn)行調(diào)用的:
// 注冊(cè)React事件 registerSimpleEvents(); registerEvents$2(); registerEvents$1(); registerEvents$3(); registerEvents();
React事件就是在組件中調(diào)用的onClick這種寫法的事件。上面分為5個(gè)函數(shù)寫,主要是區(qū)分不同的事件注冊(cè)邏輯,但是最后都會(huì)添加到allNativeEvents的Set數(shù)據(jù)結(jié)構(gòu)中。
registerSimpleEvents
這里會(huì)注冊(cè)大部分事件,它們?cè)?code>React被定義為頂級(jí)事件。
它們分為三類:
- 離散事件:
discreteEvent,常見的如:click, keyup, change; - 用戶阻塞事件:
userBlocking,常見的如:dragEnter, mouseMove, scroll; - 連續(xù)事件:
continuous,常見的如:error, progress, load,; 它們的優(yōu)先級(jí)排序:
0:離散事件, 1:用戶阻塞事件, 2:連續(xù)事件
它們會(huì)注冊(cè)冒泡和捕獲階段兩個(gè)事件。
registerEvents$2
注冊(cè)類似onMouseEnter,onMouseLeave單階段事件,只注冊(cè)冒泡階段事件。
registerEvents$1
注冊(cè)onChange相關(guān)事件,注冊(cè)冒泡和捕獲階段兩個(gè)事件。
registerEvents$3
注冊(cè)onSelect相關(guān)事件,注冊(cè)冒泡和捕獲階段兩個(gè)事件。
registerEvents
注冊(cè)onBeforeInput,onCompositionUpdate等相關(guān)事件,注冊(cè)冒泡和捕獲階段兩個(gè)事件。相關(guān)參考視頻講解:進(jìn)入學(xué)習(xí)
事件監(jiān)聽
在React源碼系列之二:React的渲染機(jī)制曾提到過,React在開始渲染前,會(huì)為應(yīng)用創(chuàng)建一個(gè)fiberRoot作為應(yīng)用的根節(jié)點(diǎn)。在創(chuàng)建fiberRoot還會(huì)做一件事,就是
listenToAllSupportedEvents(rootContainerElement);
從字面就能理解這個(gè)函數(shù)是做事件監(jiān)聽的,其中rootContainerElement參數(shù)就是應(yīng)用中的id = root的DOM元素。
該函數(shù)主要遍歷上面事件注冊(cè)添加到allNativeEvents的事件,按照一定規(guī)則,區(qū)分冒泡階段,捕獲階段,區(qū)分有無副作用進(jìn)行監(jiān)聽,監(jiān)聽的api還是addEventListener:
// 監(jiān)聽冒泡階段事件
function addEventBubbleListener(target, eventType, listener) {
target.addEventListener(eventType, listener, false);
return listener;
}
// 監(jiān)聽捕獲階段事件
function addEventCaptureListener(target, eventType, listener) {
target.addEventListener(eventType, listener, true);
return listener;
}代碼中的target就是id = root的DOM元素。
注意,上面監(jiān)聽的listener是一個(gè)事件派發(fā)器,并不是真實(shí)的瀏覽器事件或你寫的事件回調(diào)函數(shù)。 不要搞混淆了。
事件派發(fā)
上面提到,事件一旦在id = root的DOM元素中委托,其實(shí)是一直在觸發(fā)的,只是沒有綁定對(duì)應(yīng)的回調(diào)函數(shù)。
意思是,當(dāng)我們把鼠標(biāo)移入我們的應(yīng)用頁(yè)面中時(shí),這時(shí)就在派發(fā)事件了,因?yàn)轫?yè)面的DOM元素是有監(jiān)聽mousemove之類的事件的。
那問題來了,React是如何得知我們給事件綁定了回調(diào)函數(shù)并觸發(fā)對(duì)應(yīng)的回調(diào)函數(shù)的?
帶著這個(gè)問題我們來研究下事件派發(fā)。
要講事件派發(fā),還得提下事件監(jiān)聽階段監(jiān)聽的listener,它實(shí)際是下面這玩意:
function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
var eventPriority = getEventPriorityForPluginSystem(domEventName);
var listenerWrapper;
switch (eventPriority) {
case DiscreteEvent:
listenerWrapper = dispatchDiscreteEvent;
break;
case UserBlockingEvent:
listenerWrapper = dispatchUserBlockingUpdate;
break;
case ContinuousEvent:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}和事件注冊(cè)一樣,listener也分為dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三種。它們之間的主要區(qū)別是執(zhí)行優(yōu)先級(jí),還有discreteEvent涉及到要清除之前的discreteEvent問題,所以做了區(qū)分。但是它們最后都會(huì)調(diào)用dispatchEvent。
所以事件派發(fā)的角色應(yīng)該是dispatchEvent
function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
var allowReplay = true;
allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
// 如果有離散事件正在執(zhí)行,會(huì)排隊(duì),順序執(zhí)行
if (allowReplay && hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName)) {
domEventName, eventSystemFlags, targetContainer, nativeEvent);
return;
}
// 嘗試事件派發(fā),如果成功,就不用執(zhí)行下面的代碼了
var blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
// 嘗試事件派發(fā)成功
if (blockedOn === null) {
if (allowReplay) {
// 清除連續(xù)事件隊(duì)列
clearIfContinuousEvent(domEventName, nativeEvent);
}
return;
}
if (allowReplay) {
if (isReplayableDiscreteEvent(domEventName)) {
queueDiscreteEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent);
return;
}
if (queueIfContinuousEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent)) {
return;
}
clearIfContinuousEvent(domEventName, nativeEvent);
}
dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}介紹下dispatchEvent的幾個(gè)參數(shù):
domEventName: DOM事件名稱,如:click,不是onClick;eventSystemFlags:事件系統(tǒng)標(biāo)記;targetContainer:id=root的DOM元素;nativeEvent:原生事件(來自addEventListener);
在attemptToDispatchEvent中, 根據(jù)nativeEvent.target找到真正觸發(fā)事件的DOM元素,并根據(jù)DOM元素找到對(duì)應(yīng)的fiber節(jié)點(diǎn),判斷fiber節(jié)點(diǎn)的類型以及是否已渲染來決定是否要派發(fā)事件。
在一系列判斷通過后,就開始真正的事件處理了:
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
// 獲取觸發(fā)事件的DOM元素
var nativeEventTarget = getEventTarget(nativeEvent);
// 初始化事件派發(fā)隊(duì)列
var dispatchQueue = [];
// 合成事件
extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
// 按事件派發(fā)隊(duì)列執(zhí)行事件派發(fā)
processDispatchQueue(dispatchQueue, eventSystemFlags);
}在extractEvents$5中會(huì)進(jìn)行事件合成,放在下面單獨(dú)講。
在processDispatchQueue會(huì)根據(jù)事件階段(冒泡或捕獲)來決定是正序還是倒序遍歷合成事件中的listeners。
接下來就比較簡(jiǎn)單了。 遍歷listeners執(zhí)行上面的listener。
合成事件
在合成事件中,會(huì)根據(jù)domEventName來決定使用哪種類型的合成事件。
以click為例,當(dāng)我們點(diǎn)擊頁(yè)面的某個(gè)元素時(shí),React會(huì)根據(jù)原生事件nativeEvent找到觸發(fā)事件的DOM元素和對(duì)應(yīng)的fiber節(jié)點(diǎn)。并以該節(jié)點(diǎn)為孩子節(jié)點(diǎn)往上查找,找到包括該節(jié)點(diǎn)及以上所有的click回調(diào)函數(shù)創(chuàng)建dispatchListener,并添加到listeners數(shù)組中。
// dispatchListener
{
instance: instance, // 事件所在的fiber節(jié)點(diǎn)
listener: listener, // 事件回調(diào)函數(shù)
currentTarget: currentTarget // 事件對(duì)應(yīng)的DOM元素
}當(dāng)向上查找完成后,會(huì)基于click類型的合成事件類創(chuàng)建事件
// 創(chuàng)建合成事件實(shí)例
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
// 事件派發(fā)隊(duì)列添加事件
dispatchQueue.push({
event: _event, // 合成事件實(shí)例
listeners: _listeners // 同類型事件的集合數(shù)組
});看下SyntheticEventCtor
// Interface根據(jù)事件類型有所不同
function createSyntheticEvent(Interface) {
// 合成事件構(gòu)造函數(shù)
function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
this._reactName = reactName;
this._targetInst = targetInst;
this.type = reactEventType;
this.nativeEvent = nativeEvent;
this.target = nativeEventTarget;
this.currentTarget = null;
// React根據(jù)不同事件類型寫了對(duì)應(yīng)的屬性接口,這里基于接口將原生事件上的屬性clone到構(gòu)造函數(shù)中
for (var _propName in Interface) {... }
var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;
if (defaultPrevented) {
this.isDefaultPrevented = functionThatReturnsTrue;
} else {
this.isDefaultPrevented = functionThatReturnsFalse;
}
this.isPropagationStopped = functionThatReturnsFalse;
return this;
}
_assign(SyntheticBaseEvent.prototype, {
// 阻止默認(rèn)事件
preventDefault: function () {...},
// 阻止捕獲和冒泡階段中當(dāng)前事件的進(jìn)一步傳播
stopPropagation: function () {...},
// 合成事件不使用對(duì)象池了,這個(gè)事件是空的,沒有意義,保存是為了向下兼容不報(bào)錯(cuò)。
persist: function () {},
isPersistent: functionThatReturnsTrue
});
return SyntheticBaseEvent;
}看到這里,我們基本能弄明白合成事件是個(gè)什么東西了。
React合成事件是將同類型的事件找出來,基于這個(gè)類型的事件,React通過代碼定義好的類型事件的接口和原生事件創(chuàng)建相應(yīng)的合成事件實(shí)例,并重寫了preventDefault和stopPropagation方法。
這樣,同類型的事件會(huì)復(fù)用同一個(gè)合成事件實(shí)例對(duì)象,節(jié)省了單獨(dú)為每一個(gè)事件創(chuàng)建事件實(shí)例對(duì)象的開銷,這就是事件的合成。
捕獲和冒泡
事件派發(fā)分為兩個(gè)階段執(zhí)行, 捕獲階段和冒泡階段。
在上面事件合成中講過,React會(huì)根據(jù)事件觸發(fā)的fiber節(jié)點(diǎn)向上查找,將上面的同類型事件添加到隊(duì)列中,這樣天然就有了一個(gè)冒泡的順序,從最底層向上冒泡。如果倒序過來遍歷就是捕獲的順序。
所以,React實(shí)現(xiàn)冒泡和捕獲就很簡(jiǎn)單了,只需要根據(jù)事件派發(fā)的階段,判斷是冒泡階段還是捕獲階段來決定是正序遍歷listeners還是倒序遍歷就行了。
總結(jié)
說是講React的合成事件,實(shí)際上講了React的事件系統(tǒng)。做下總結(jié):
React的事件系統(tǒng)分為幾個(gè)部分:
1.事件注冊(cè);
2.事件監(jiān)聽;
3.事件合成;
4.事件派發(fā); 事件系統(tǒng)流程:
- 在
React代碼執(zhí)行時(shí),內(nèi)部會(huì)自動(dòng)執(zhí)行事件的注冊(cè); - 第一次渲染,創(chuàng)建
fiberRoot時(shí),會(huì)進(jìn)行事件的監(jiān)聽,所有的事件通過addEventListener委托在id=root的DOM元素上進(jìn)行監(jiān)聽; - 在我們觸發(fā)事件時(shí),會(huì)進(jìn)行事件合成,同類型事件復(fù)用一個(gè)合成事件類實(shí)例對(duì)象;
- 最后進(jìn)行事件的派發(fā),執(zhí)行我們代碼中的事件回調(diào)函數(shù);
到此這篇關(guān)于深入分析React源碼中的合成事件的文章就介紹到這了,更多相關(guān)React合成事件內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React Native中NavigatorIOS組件的簡(jiǎn)單使用詳解
這篇文章主要介紹了React Native中NavigatorIOS組件的簡(jiǎn)單使用詳解,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-01-01
React?Refs?的使用forwardRef?源碼示例解析
這篇文章主要為大家介紹了React?之?Refs?的使用和?forwardRef?的源碼解讀,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-11-11
解讀react的onClick自動(dòng)觸發(fā)等相關(guān)問題
這篇文章主要介紹了解讀react的onClick自動(dòng)觸發(fā)等相關(guān)問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-02-02
react18中react-redux狀態(tài)管理的實(shí)現(xiàn)
本文主要介紹了react18中react-redux狀態(tài)管理的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-05-05

