深入學(xué)習(xí) React 合成事件
翁斌斌,微醫(yī)云前端工程師,在程序員的修煉道路上永不止步。
以下分析基于React, ReactDOM 16.13.1版本
提出問題
我們借鑒一個(gè)比較典型的案例開始來分析React事件
export?default?class?Dialog?extends?React.PureComponent?{
??state?=?{
????showBox:?false
??};
??componentDidMount()?{
????document.addEventListener("click",?this.handleClickBody,?false);
??}
??handleClickBody?=?()?=>?{
????this.setState({
??????showBox:?false
????});
??};
??handleClickButton?=?(e)?=>?{
????e.nativeEvent.stopPropagation();
????this.setState({
??????showBox:?true
????});
??};
??render()?{
????return?(
??????<div>
????????<button?onClick={this.handleClickButton}>點(diǎn)擊我顯示彈窗button>
????????{this.state.showBox?&&?(
??????????<div?onClick={(e)?=>?e.nativeEvent.stopPropagation()}>我是彈窗div>
????????)}
??????div>
????);
??}
}
從上面的代碼里我們不難看出我們想要做一個(gè)點(diǎn)擊某一個(gè)按鈕來展示一個(gè)模態(tài)框,并且在點(diǎn)擊除了模態(tài)框區(qū)域以外的位置希望能夠關(guān)閉這個(gè)模態(tài)框。
但是實(shí)際運(yùn)行結(jié)果和我們所想的完全不一樣,點(diǎn)擊了button按鈕并沒有任何反應(yīng),這就需要從React的合成事件說起了,讓我們分析完React的合成事件
后能夠完全的來解答這個(gè)問題。
demo地址:https://codesandbox.io/s/event-uww15?file=/src/App.tsx:0-690
合成事件的特性
React自行實(shí)現(xiàn)了一套事件系統(tǒng),主要特性有以下
自行實(shí)現(xiàn)了一套事件捕獲到事件冒泡的邏輯, 抹平各個(gè)瀏覽器之前的兼容性問題。 使用對(duì)象池來管理合成事件對(duì)象的創(chuàng)建和銷毀,可以減少垃圾回收次數(shù),防止內(nèi)存抖動(dòng)。 事件只在document上綁定,并且每種事件只綁定一次,減少內(nèi)存開銷。
首先我們先拋開上面那個(gè)按鈕,用下面這個(gè)十分簡(jiǎn)單的案例來了解React的事件使用。
function?App()?{
??function?handleButtonLog(e:?React.MouseEvent )?{
????console.log(e.currentTarget);
??}
??function?handleDivLog(e:?React.MouseEvent )?{
????console.log(e.currentTarget);
??}
??function?handleH1Log(e:?React.MouseEvent )?{
????console.log(e.currentTarget);
??}
??return?(
????<div?onClick={handleDivLog}>
??????<h1?onClick={handleH1Log}>
????????<button?onClick={handleButtonLog}>clickbutton>
??????h1>
????div>
??);
}
上面的代碼運(yùn)行后,會(huì)在控制臺(tái)中分別打印出,button, h1, div三個(gè)dom節(jié)點(diǎn),我們來研究一下他是如何工作的。
事件綁定
首先來確認(rèn)事件是如何綁定到dom節(jié)點(diǎn)上的,我們知道App組件內(nèi)的jsx代碼會(huì)通過React.CreateElement函數(shù)返回jsx對(duì)象,其中我們的onClick事件是儲(chǔ)存在每一個(gè)jsx對(duì)象的props屬性內(nèi),通過一系列方法得知在React在reconciliation階段中會(huì)把jsx對(duì)象轉(zhuǎn)換為fiber對(duì)象,這里有一個(gè)方法叫做completeWork,
function?completeWork(current,?workInProgress,?renderExpirationTime)?{
????//?只保留關(guān)鍵代碼
????case?HostComponent:
??????{
????????popHostContext(workInProgress);
????????var?rootContainerInstance?=?getRootHostContainer();
????????var?type?=?workInProgress.type;
????????if?(current?!==?null?&&?workInProgress.stateNode?!=?null)?{
??????????//?更新
????????}?else?{
??????????//?創(chuàng)建
??????????if?(_wasHydrated)?{
????????????//?ssr情況
??????????}?else?{
????????????var?instance?=?createInstance(type,?newProps,?rootContainerInstance,?currentHostContext,?workInProgress);
????????????//?初始化DOM節(jié)點(diǎn)
????????????if?(finalizeInitialChildren(instance,?type,?newProps,?rootContainerInstance))?{
????????????}
??????????}
????????}
}
這個(gè)函數(shù)內(nèi)通過createInstance創(chuàng)建dom實(shí)例,并且調(diào)用finalizeInitialChildren函數(shù),在finalizeInitialChildren函數(shù)中會(huì)把props設(shè)置到真實(shí)的dom節(jié)點(diǎn)上,這里如果遇到類似onClick,onChange的props時(shí),會(huì)觸發(fā)事件綁定的邏輯。
//?進(jìn)行事件綁定
ensureListeningTo(rootContainerElement,?propKey);
function?ensureListeningTo(rootContainerElement,?registrationName)?{
??//?忽略無關(guān)代碼
??var?doc?=?isDocumentOrFragment???rootContainerElement?:?rootContainerElement.ownerDocument;
??legacyListenToEvent(registrationName,?doc);
}
在ensureListeningTo函數(shù)中會(huì)通過實(shí)際觸發(fā)事件的節(jié)點(diǎn),去尋找到它的document節(jié)點(diǎn),并且調(diào)用legacyListenToEvent函數(shù)來進(jìn)行事件綁定
function?legacyListenToEvent(registrationName,?mountAt)?{
??var?listenerMap?=?getListenerMapForElement(mountAt);
??var?dependencies?=?registrationNameDependencies[registrationName];
??for?(var?i?=?0;?i?????var?dependency?=?dependencies[i];
????legacyListenToTopLevelEvent(dependency,?mountAt,?listenerMap);
??}
}
registrationNameDependencies數(shù)據(jù)結(jié)構(gòu)如下

在legacyListenToEvent函數(shù)中首先通過獲取document節(jié)點(diǎn)上監(jiān)聽的事件名稱Map對(duì)象,然后去通過綁定在jsx上的事件名稱,例如onClick來獲取到真實(shí)的事件名稱,例如click,依次進(jìn)行legacyListenToTopLevelEvent方法的調(diào)用
function?legacyListenToTopLevelEvent(topLevelType,?mountAt,?listenerMap)?{
??//?只保留主邏輯
??//?相同的事件只綁定一次
??if?(!listenerMap.has(topLevelType))?{
????switch?(topLevelType)?{
??????//?根據(jù)事件類型進(jìn)行捕獲或者冒泡綁定
??????case?TOP_SCROLL:
????????trapCapturedEvent(XX);
??????default:
????????trapBubbledEvent(topLevelType,?mountAt)
????????break;
????}
????listenerMap.set(topLevelType,?null);
??}
}
//?無論是trapBubbledEvent還是trapCapturedEvent都是調(diào)用trapEventForPluginEventSystem
//?區(qū)別就是第三個(gè)參數(shù)是ture還是false用來對(duì)應(yīng)addEventListener中的第三個(gè)參數(shù)
function?trapBubbledEvent(topLevelType,?element)?{
??trapEventForPluginEventSystem(element,?topLevelType,?false);
}
function?trapCapturedEvent(topLevelType,?element)?{
??trapEventForPluginEventSystem(element,?topLevelType,?true);
}
legacyListenToTopLevelEvent函數(shù)做了以下兩件事
是否在document上已經(jīng)綁定過原始事件名,已經(jīng)綁定過則直接退出,未綁定則綁定結(jié)束以后把事件名稱設(shè)置到Map對(duì)象上,再下一次綁定相同的事件時(shí)直接跳過。 根據(jù)事件是否能冒泡來來進(jìn)行捕獲階段的綁定或者冒泡階段的綁定。
function?trapEventForPluginEventSystem(container,?topLevelType,?capture)?{
??var?listener;
??switch?(getEventPriorityForPluginSystem(topLevelType))?{
????case?DiscreteEvent:
??????listener?=?dispatchDiscreteEvent.bind(null,?topLevelType,?PLUGIN_EVENT_SYSTEM,?container);
??????break;
????case?UserBlockingEvent:
??????listener?=?dispatchUserBlockingUpdate.bind(null,?topLevelType,?PLUGIN_EVENT_SYSTEM,?container);
??????break;
????case?ContinuousEvent:
????default:
??????listener?=?dispatchEvent.bind(null,?topLevelType,?PLUGIN_EVENT_SYSTEM,?container);
??????break;
??}
??var?rawEventName?=?getRawEventName(topLevelType);
??if?(capture)?{
????addEventCaptureListener(container,?rawEventName,?listener);
??}?else?{
????addEventBubbleListener(container,?rawEventName,?listener);
??}
}
到目前為止我們已經(jīng)拿到了真實(shí)的事件名稱和綁定在事件的哪個(gè)階段,剩下就還有一個(gè)監(jiān)聽事件本身了,這一步會(huì)在trapEventForPluginEventSystem函數(shù)內(nèi)被獲取到,他會(huì)通過事件的優(yōu)先級(jí)來獲取不同的監(jiān)聽事件,這部分會(huì)和調(diào)度方面有相關(guān),我們只需要知道最終實(shí)際綁定的都是dispatchEvent這個(gè)監(jiān)聽事件,然后調(diào)用瀏覽器的addEventListener事件來綁定上dispatchEvent函數(shù)
到此為止事件的綁定暫時(shí)告一段落了,從上面能得出幾個(gè)結(jié)論。
事件都是綁定在document上的。 jsx中的事件名稱會(huì)經(jīng)過處理,處理后的事件名稱才會(huì)被綁定,例如onClick會(huì)使用click這個(gè)名稱來綁定。 不管用什么事件來綁定, 他們的監(jiān)聽事件并不是傳入jsx的事件函數(shù),而是會(huì)根據(jù)事件的優(yōu)先級(jí)來綁定 dispatchDiscreteEvent,dispatchUserBlockingUpdate或者dispatchEvent三個(gè)監(jiān)聽函數(shù)之一,但是最終在觸發(fā)事件調(diào)用的還是dispatchEvent事件。
事件觸發(fā)
從事件綁定得知我們點(diǎn)擊的button按鈕的時(shí)候,觸發(fā)的回調(diào)函數(shù)并不是實(shí)際的回調(diào)函數(shù),而是dispatchEvent函數(shù),
所以我們通常會(huì)有幾個(gè)疑問
它是怎么獲取到用戶事件的回調(diào)函數(shù)的? 為什么在合成事件對(duì)象不能被保存下來,而需要調(diào)用特殊的函數(shù)才能保留? 合成事件是怎么創(chuàng)建出來的?
function?dispatchEventForLegacyPluginEventSystem(topLevelType,?eventSystemFlags,?nativeEvent,?targetInst)?{
??var?bookKeeping?=?getTopLevelCallbackBookKeeping(topLevelType,?nativeEvent,?targetInst,?eventSystemFlags);
??try?{
????batchedEventUpdates(handleTopLevel,?bookKeeping);
??}?finally?{
????releaseTopLevelCallbackBookKeeping(bookKeeping);
??}
}
接下來的分析中我們就來解決這幾個(gè)問題,首先看到dispatchEvent函數(shù),忽略掉其他分支會(huì)發(fā)現(xiàn)實(shí)際調(diào)用的是dispatchEventForLegacyPluginEventSystem函數(shù), 他首先通過callbackBookkeepingPool中獲取一個(gè)bookKeeping對(duì)象,然后調(diào)用handleTopLevel函數(shù),在調(diào)用結(jié)束的時(shí)候吧bookKeeping對(duì)象放回到callbackBookkeepingPool中,實(shí)現(xiàn)了內(nèi)存復(fù)用。
bookKeeping對(duì)象的結(jié)構(gòu)如圖

//?忽略分支代碼,只保留主流程
function?handleTopLevel(bookKeeping)?{
??var?targetInst?=?bookKeeping.targetInst;
??var?ancestor?=?targetInst;
??do?{
????var?tag?=?ancestor.tag;
????if?(tag?===?HostComponent?||?tag?===?HostText)?{
??????bookKeeping.ancestors.push(ancestor);
????}
??}?while?(ancestor);
??for?(var?i?=?0;?i?????targetInst?=?bookKeeping.ancestors[i];
????runExtractedPluginEventsInBatch(topLevelType,?targetInst,?nativeEvent,?eventTarget,?eventSystemFlags);
??}
}
在handleTopLevel函數(shù)內(nèi),通過首先把觸發(fā)事件的節(jié)點(diǎn)如果是dom節(jié)點(diǎn)或者文字節(jié)點(diǎn)的話,那就把對(duì)應(yīng)的fiber對(duì)象放入bookkeeping.ancestors的數(shù)組內(nèi),接下去依次獲取bookKeeping.ancestors上的每一個(gè)fiber對(duì)象,通過runExtractedPluginEventsInBatch函數(shù)來創(chuàng)建合成事件對(duì)象。
function?runExtractedPluginEventsInBatch(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags)?{
??var?events?=?extractPluginEvents(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags);
??runEventsInBatch(events);
}
在runExtractedPluginEventsInBatch中會(huì)通過調(diào)用extractPluginEvents函數(shù),在這個(gè)函數(shù)內(nèi)通過targetInst這個(gè)fiber對(duì)象,從這個(gè)對(duì)象一直往上尋找,尋找有一樣的事件綁定的節(jié)點(diǎn),并且把他們的回調(diào)事件組合到合成事件對(duì)象上,這里先討論事件觸發(fā)的流程,所以先簡(jiǎn)單帶過合成事件是如何生成的以及是如何去尋找到需要被觸發(fā)的事件, 后面會(huì)詳細(xì)的講解合成事件,最后在拿到合成事件以后調(diào)用runEventsInBatch函數(shù)
function?runEventsInBatch(events)?{
??forEachAccumulated(processingEventQueue,?executeDispatchesAndReleaseTopLevel);
}
其中processingEventQueue是多個(gè)事件列表,我們這只有一個(gè)事件隊(duì)列,forEachAccumulated它的目的是為了按照隊(duì)列的順序去執(zhí)行多個(gè)事件,在我們的例子中其實(shí)就相當(dāng)于executeDispatchesAndReleaseTopLevel(processingEventQueue),接下來就是調(diào)用到executeDispatchesAndRelease,從名稱就看出來他是首先執(zhí)行事件,然后對(duì)事件對(duì)象進(jìn)行釋放
var?executeDispatchesAndRelease?=?function?(event)?{
??if?(event)?{
????executeDispatchesInOrder(event);
????if?(!event.isPersistent())?{
??????event.constructor.release(event);
????}
??}
};
代碼很少,首先調(diào)用executeDispatchesInOrder來傳入合成事件,在里面按照順序去執(zhí)行合成事件對(duì)象上的回調(diào)函數(shù),如果有多個(gè)回調(diào)函數(shù),在執(zhí)行每個(gè)回調(diào)函數(shù)的時(shí)候還會(huì)去判斷event.isPropagationStopped()的狀態(tài),之前有函數(shù)調(diào)用了合成事件的stopPropagation函數(shù)的話,就停止執(zhí)行后續(xù)的回調(diào),但是要注意的時(shí)候這里的dispatchListeners[i]函數(shù)并不是用戶傳入的回調(diào)函數(shù),而是經(jīng)過包裝的事件,這塊會(huì)在合成事件的生成中介紹,在事件執(zhí)行結(jié)束后React還會(huì)去根據(jù)用戶是否調(diào)用了event.persist()函數(shù)來決定是否保留這次的事件對(duì)象是否要回歸事件池,如果未被調(diào)用,該事件對(duì)象上的狀態(tài)會(huì)被重置,至此事件觸發(fā)已經(jīng)完畢。
合成事件的生成
從事件監(jiān)聽的流程中我們知道了合成事件是從extractPluginEvents創(chuàng)建出來的,那么看一下extractPluginEvents的代碼
function?extractPluginEvents(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags)?{
??var?events?=?null;
??for?(var?i?=?0;?i?????var?possiblePlugin?=?plugins[i];
????if?(possiblePlugin)?{
??????var?extractedEvents?=?possiblePlugin.extractEvents(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags);
??????if?(extractedEvents)?{
????????events?=?accumulateInto(events,?extractedEvents);
??????}
????}
??}
??return?events;
}
首先來了解一下plugins是個(gè)什么東西,由于React會(huì)服務(wù)于不同的平臺(tái),所以每個(gè)平臺(tái)的事件會(huì)用插件的形式來注入到React中,例如瀏覽器就是ReactDOM中進(jìn)行注入
injectEventPluginsByName({
??SimpleEventPlugin:?SimpleEventPlugin,
??EnterLeaveEventPlugin:?EnterLeaveEventPlugin,
??ChangeEventPlugin:?ChangeEventPlugin,
??SelectEventPlugin:?SelectEventPlugin,
??BeforeInputEventPlugin:?BeforeInputEventPlugin,
});
injectEventPluginsByName函數(shù)會(huì)通過一些操作把事件插件注冊(cè)到plugins對(duì)象上,數(shù)據(jù)結(jié)構(gòu)如下

所以會(huì)依次遍歷plugin,調(diào)用plugin上的extractEvents函數(shù)來嘗試是否能夠生成出合成事件對(duì)象,在我們的例子中用的是click事件,那么它會(huì)進(jìn)入到SimpleEventPlugin.extractEvents函數(shù)
var?SimpleEventPlugin?=?{
??extractEvents:?function?(topLevelType,?targetInst,?nativeEvent,?nativeEventTarget,?eventSystemFlags)?{
????var?EventConstructor;
????switch?(topLevelType)?{
??????case?TOP_KEY_DOWN:
??????case?TOP_KEY_UP:
????????EventConstructor?=?SyntheticKeyboardEvent;
????????break;
??????case?TOP_BLUR:
??????case?TOP_FOCUS:
????????EventConstructor?=?SyntheticFocusEvent;
????????break;
??????default:
????????EventConstructor?=?SyntheticEvent;
????????break;
????}
????var?event?=?EventConstructor.getPooled(dispatchConfig,?targetInst,?nativeEvent,?nativeEventTarget);
????accumulateTwoPhaseDispatches(event);
????return?event;
??}
};
這個(gè)函數(shù)是通過topLevelType的類型來獲取合成事件的構(gòu)造函數(shù),例如代碼中的SyntheticKeyboardEvent,SyntheticFocusEvent等都是SyntheticEvent的子類,在基礎(chǔ)上附加了自己事件的特殊屬性,我們的click事件會(huì)使用到SyntheticEvent這個(gè)構(gòu)造函數(shù),然后通過getPooled函數(shù)來創(chuàng)建或者從事件池中取出一個(gè)合成事件對(duì)象實(shí)例。然后在accumulateTwoPhaseDispatchesSingle函數(shù)中,按照捕獲到冒泡的順序來獲取所有的事件回調(diào)
function?accumulateTwoPhaseDispatchesSingle(event)?{
??if?(event?&&?event.dispatchConfig.phasedRegistrationNames)?{
????traverseTwoPhase(event._targetInst,?accumulateDirectionalDispatches,?event);
??}
}
function?traverseTwoPhase(inst,?fn,?arg)?{
??var?path?=?[];
??while?(inst)?{
????path.push(inst);
????inst?=?getParent(inst);
??}
??var?i;
??for?(i?=?path.length;?i--?>?0;)?{
????fn(path[i],?'captured',?arg);
??}
??for?(i?=?0;?i?????fn(path[i],?'bubbled',?arg);
??}
}
traverseTwoPhase函數(shù)會(huì)從當(dāng)前的fiber節(jié)點(diǎn)通過return屬性,找到所有的是原生DOM節(jié)點(diǎn)的fiber對(duì)象,然后推入到列表中,我們的例子中就是[ButtonFiber, H1Fiber, DivFiber], 首先執(zhí)行捕獲階段的循環(huán),從后往前執(zhí)行,接著從前往后執(zhí)行冒泡的循環(huán),對(duì)應(yīng)了瀏覽器原始的事件觸發(fā)流程,最后會(huì)往accumulateDirectionalDispatches函數(shù)中傳入當(dāng)前執(zhí)行的fiber和事件執(zhí)行的階段。
function?listenerAtPhase(inst,?event,?propagationPhase)?{
??var?registrationName?=?event.dispatchConfig.phasedRegistrationNames[propagationPhase];
??return?getListener(inst,?registrationName);
}
function?accumulateDirectionalDispatches(inst,?phase,?event)?{
??var?listener?=?listenerAtPhase(inst,?event,?phase);
??if?(listener)?{
????event._dispatchListeners?=?accumulateInto(event._dispatchListeners,?listener);
????event._dispatchInstances?=?accumulateInto(event._dispatchInstances,?inst);
??}
}
listenerAtPhase中首先通過原生事件名和當(dāng)前執(zhí)行的階段(捕獲,還是冒泡)去再去獲取對(duì)應(yīng)的props事件名稱(onClick,onClickCapture),然后通過React事件名稱去fiber節(jié)點(diǎn)上獲取到相應(yīng)的事件回調(diào)函數(shù),最后拼接在合成對(duì)象的_dispatchListeners數(shù)組內(nèi),當(dāng)全部節(jié)點(diǎn)運(yùn)行結(jié)束以后_dispatchListeners對(duì)象上就會(huì)有三個(gè)回調(diào)函數(shù)[handleButtonLog, handleH1Log, handleDivLog],這里的回調(diào)函數(shù)就是我們?cè)诮M件內(nèi)定義的真實(shí)事件的回調(diào)函數(shù)。
到此合成事件構(gòu)造就完成了,主要做了三件事:
通過事件名稱去選擇合成事件的構(gòu)造函數(shù), 事件去獲取到組件上事件綁定的回調(diào)函數(shù)設(shè)置到合成事件上的 _dispatchListeners屬性上,用于事件觸發(fā)的時(shí)候去調(diào)用。還有就是在初始化的時(shí)候去注入平臺(tái)的事件插件。
事件解綁
通常我們寫事件綁定的時(shí)候會(huì)在頁(yè)面卸載的時(shí)候進(jìn)行事件的解綁,但是在React中,框架本身由于只會(huì)在document上進(jìn)行每種事件最多一次的綁定,所以并不會(huì)進(jìn)行事件的解綁。
批量更新
當(dāng)然如果我們使用React提供的事件,而不是使用我們自己綁定的原生事件除了會(huì)進(jìn)行事件委托以外還有什么優(yōu)勢(shì)呢? 再來看一個(gè)例子
export?default?class?EventBatchUpdate?extends?React.PureComponent<>?{
??button?=?null;
??constructor(props)?{
????super(props);
????this.state?=?{
??????count:?0
????};
????this.button?=?React.createRef();
??}
??componentDidMount()?{
????this.button.current.addEventListener(
??????"click",
??????this.handleNativeClickButton,
??????false
????);
??}
??handleNativeClickButton?=?()?=>?{
????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
??};
??handleClickButton?=?()?=>?{
????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
??};
??render()?{
????console.log("update");
????return?(
??????<div>
????????<h1>legacy?eventh1>
????????<button?ref={this.button}>native?event?addbutton>
????????<button?onClick={this.handleClickButton}>React?event?addbutton>
????????{this.state.count}
??????div>
????);
??}
}
在線demo地址:https://codesandbox.io/s/legacy-event-kjngx?file=/src/App.tsx:0-1109

首先點(diǎn)擊第一個(gè)按鈕,發(fā)現(xiàn)有兩個(gè)update被打印出,意味著被render了兩次。

點(diǎn)擊第二個(gè)按鈕,只有一個(gè)update被打印出來。
會(huì)發(fā)現(xiàn)通過React事件內(nèi)多次調(diào)用setState,會(huì)自動(dòng)合并多個(gè)setState,但是在原生事件綁定上默認(rèn)并不會(huì)進(jìn)行合并多個(gè)setState,那么有什么手段能解決這個(gè)問題呢?
通過 batchUpdate函數(shù)來手動(dòng)聲明運(yùn)行上下文。
??handleNativeClickButton?=?()?=>?{
????ReactDOM.unstable_batchedUpdates(()?=>?{
??????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
??????this.setState((preState)?=>?({?count:?preState.count?+?1?}));
????});
??};
在線demo地址:https://codesandbox.io/s/legacy-eventbatchupdate-smisq?file=/src/App.tsx:519-749

首先點(diǎn)擊第一個(gè)按鈕,只有一個(gè)update被打印出來。

點(diǎn)擊第二個(gè)按鈕,還是只有一個(gè)update被打印出來。
啟用 concurrent mode的情況。(目前不推薦,未來的方案)
import?ReactDOM?from?"React-dom";
const?root?=?ReactDOM.unstable_createRoot(document.getElementById("root"));
root.render(<App?/>);
在線demo地址:https://codesandbox.io/s/concurrentevent-9oxoi?file=/src/index.js:0-224
會(huì)發(fā)現(xiàn)不需要修改任何代碼,只需要開啟
concurrent mode,就會(huì)自動(dòng)進(jìn)行setState的合并。

首先點(diǎn)擊第一個(gè)按鈕,只有一個(gè)update被打印出來。

點(diǎn)擊第二個(gè)按鈕,還是只有一個(gè)update被打印出來。
React17中的事件改進(jìn)
在最近發(fā)布的React17版本中,對(duì)事件系統(tǒng)了一些改動(dòng),和16版本里面的實(shí)現(xiàn)有了一些區(qū)別,我們就來了解一下17中更新的點(diǎn)。
更改事件委托
首先第一個(gè)修改點(diǎn)就是更改了事件委托綁定節(jié)點(diǎn),在16版本中,React都會(huì)把事件綁定到頁(yè)面的document元素上,這在多個(gè)React版本共存的情況下就會(huì)雖然某個(gè)節(jié)點(diǎn)上的函數(shù)調(diào)用了 e.stopPropagation(),但還是會(huì)導(dǎo)致另外一個(gè)React版本上綁定的事件沒有被阻止觸發(fā),所以在17版本中會(huì)把事件綁定到render函數(shù)的節(jié)點(diǎn)上。
去除事件池
17版本中移除了 event pooling,這是因?yàn)?React 在舊瀏覽器中重用了不同事件的事件對(duì)象,以提高性能,并將所有事件字段在它們之前設(shè)置為 null。在 React 16 及更早版本中,使用者必須調(diào)用e.persist()才能正確的使用該事件,或者正確讀取需要的屬性。
對(duì)標(biāo)瀏覽器
onScroll事件不再冒泡,以防止出現(xiàn)常見的混淆。React 的 onFocus和onBlur事件已在底層切換為原生的focusin和focusout事件。它們更接近 React 現(xiàn)有行為,有時(shí)還會(huì)提供額外的信息。捕獲事件(例如, onClickCapture)現(xiàn)在使用的是實(shí)際瀏覽器中的捕獲監(jiān)聽器。
問題解答
現(xiàn)在讓我們回到最開始的例子中,來看這個(gè)問題如何被修復(fù)
. ?16版本修復(fù)方法一
??handleClickButton?=?(e:?React.MouseEvent)?=>?{
????e.nativeEvent.stopImmediatePropagation();
????...
??};
我們知道React事件綁定的時(shí)刻是在reconciliation階段,會(huì)在原生事件的綁定前,那么可以通過調(diào)用e.nativeEvent.stopImmediatePropagation();
來進(jìn)行document后續(xù)事件的阻止。
在線demo地址:https://codesandbox.io/s/v16fixevent1-wb8m7
16版本修復(fù)方法二
??window.addEventListener("click",?this.handleClickBody,?false);
另外一個(gè)方法就是在16版本中事件會(huì)被綁定在document上,所以只要把原生事件綁定在window上,并且調(diào)用e.nativeEvent.stopPropagation();來阻止事件冒泡到window上即可修復(fù)。
在線demo地址:https://codesandbox.io/s/v16fixevent2-4e2b5
React17版本修復(fù)方法
在17版本中React事件并不會(huì)綁定在document上,所以并不需要修改任何代碼,即可修復(fù)這個(gè)問題。
在線demo地址:https://codesandbox.io/s/v17fixevent-wzsw5
總結(jié)
我們通過一個(gè)經(jīng)典的例子入手,自頂而下來分析React源碼中事件的實(shí)現(xiàn)方式,了解事件的設(shè)計(jì)思想,最后給出多種的解決方案,能夠在繁雜的業(yè)務(wù)中挑選最合適的技術(shù)方案來進(jìn)行實(shí)踐。
