「React進(jìn)階」一文吃透react事件原理
一 前言
今天我們來(lái)一起探討一下React事件原理,這篇文章,我盡量用通俗簡(jiǎn)潔的方式,把React事件系統(tǒng)講的明明白白。
我們講的react版本是16.13.1 , v17之后react對(duì)于事件系統(tǒng)會(huì)有相關(guān)的改版,文章后半部分會(huì)提及。
老規(guī)矩,在正式講解react之前,我們先想想這幾個(gè)問(wèn)題(如果我是面試官,你會(huì)怎么回答?):
1 我們寫(xiě)的事件是綁定在 dom上么,如果不是綁定在哪里?2 為什么我們的事件不能綁定給組件? 3 為什么我們的事件手動(dòng)綁定 this(不是箭頭函數(shù)的情況)4 為什么不能用 return false來(lái)阻止事件的默認(rèn)行為?5 react怎么通過(guò)dom元素,找到與之對(duì)應(yīng)的fiber對(duì)象的?6 onClick是在冒泡階段綁定的?那么onClickCapture就是在事件捕獲階段綁定的嗎?

必要的知識(shí)概念
在弄清楚react事件之前,有幾個(gè)概念我們必須弄清楚,因?yàn)橹挥信靼走@幾個(gè)概念,在事件觸發(fā)階段,我們才能更好的理解react處理事件本質(zhì)。
我們寫(xiě)在JSX事件終將變成什么?
我們先寫(xiě)一段含有點(diǎn)擊事件的react JSX語(yǔ)法,看一下它最終會(huì)變成什么樣子?
class Index extends React.Component{
handerClick= (value) => console.log(value)
render(){
return <div>
<button onClick={ this.handerClick } > 按鈕點(diǎn)擊 </button>
</div>
}
}
經(jīng)過(guò)babel轉(zhuǎn)換成React.createElement形式,如下:

最終轉(zhuǎn)成fiber對(duì)象形式如下:

fiber對(duì)象上的memoizedProps 和 pendingProps保存了我們的事件。
什么是合成事件?
通過(guò)上一步我們看到了,我們聲明事件保存的位置。但是事件有沒(méi)有被真正的注冊(cè)呢?我們接下來(lái)看一下:
我們看一下當(dāng)前這個(gè)元素<button>上有沒(méi)有綁定這個(gè)事件監(jiān)聽(tīng)器呢?

button上綁定的事件
我們可以看到 ,button上綁定了兩個(gè)事件,一個(gè)是document上的事件監(jiān)聽(tīng)器,另外一個(gè)是button,但是事件處理函數(shù)handle,并不是我們的handerClick事件,而是noop。
noop是什么呢?我們接著來(lái)看。
原來(lái)noop就指向一個(gè)空函數(shù)。

然后我們看document綁定的事件

可以看到click事件被綁定在document上了。
接下來(lái)我們?cè)俑愀闶虑??????,在demo項(xiàng)目中加上一個(gè)input輸入框,并綁定一個(gè)onChange事件。睜大眼睛看看接下來(lái)會(huì)發(fā)生什么?
class Index extends React.Component{
componentDidMount(){
console.log(this)
}
handerClick= (value) => console.log(value)
handerChange=(value) => console.log(value)
render(){
return <div style={{ marginTop:'50px' }} >
<button onClick={ this.handerClick } > 按鈕點(diǎn)擊 </button>
<input placeholder="請(qǐng)輸入內(nèi)容" onChange={ this.handerChange } />
</div>
}
}
我們先看一下input dom元素上綁定的事件

然后我們看一下document上綁定的事件

我們發(fā)現(xiàn),我們給<input>綁定的onChange,并沒(méi)有直接綁定在input上,而是統(tǒng)一綁定在了document上,然后我們onChange被處理成很多事件監(jiān)聽(tīng)器,比如blur , change , input , keydown , keyup 等。
綜上我們可以得出結(jié)論:
①我們?cè)?
jsx中綁定的事件(demo中的handerClick,handerChange),根本就沒(méi)有注冊(cè)到真實(shí)的dom上。是綁定在document上統(tǒng)一管理的。②真實(shí)的
dom上的click事件被單獨(dú)處理,已經(jīng)被react底層替換成空函數(shù)。③我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">react綁定的事件,比如
onChange,在document上,可能有多個(gè)事件與之對(duì)應(yīng)。④
react并不是一開(kāi)始,把所有的事件都綁定在document上,而是采取了一種按需綁定,比如發(fā)現(xiàn)了onClick事件,再去綁定document click事件。
那么什么是react事件合成呢?
在react中,我們綁定的事件onClick等,并不是原生事件,而是由原生事件合成的React事件,比如 click事件合成為onClick事件。比如blur , change , input , keydown , keyup等 , 合成為onChange。
那么react采取這種事件合成的模式呢?
一方面,將事件綁定在document統(tǒng)一管理,防止很多事件直接綁定在原生的dom元素上。造成一些不可控的情況
另一方面, React 想實(shí)現(xiàn)一個(gè)全瀏覽器的框架, 為了實(shí)現(xiàn)這種目標(biāo)就需要提供全瀏覽器一致性的事件系統(tǒng),以此抹平不同瀏覽器的差異。
接下來(lái)的文章中,會(huì)介紹react是怎么做事件合成的。
dom元素對(duì)應(yīng)的fiber Tag對(duì)象
我們知道了react怎么儲(chǔ)存了我們的事件函數(shù)和事件合成因果。接下來(lái)我想讓大家記住一種類(lèi)型的 fiber 對(duì)象,因?yàn)楹竺鏁?huì)用到,這對(duì)后續(xù)的理解很有幫助。
我們先來(lái)看一個(gè)代碼片段:
<div>
<div> hello , my name is alien </div>
</div>
看<div> hello , my name is alien </div> 對(duì)應(yīng)的 fiber類(lèi)型。tag = 5
然后我們?nèi)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">react源碼中找到這種類(lèi)的fiber類(lèi)型。
/react-reconciler/src/ReactWorkTagsq.js
export const HostComponent = 5; // 元素節(jié)點(diǎn)
好的 ,我們暫且把 HostComponent 和 HostText記錄??下來(lái)。接下來(lái)回歸正題,我們先來(lái)看看react事件合成機(jī)制。
二 事件初始化-事件合成,插件機(jī)制
接下來(lái),我們來(lái)看一看react這么處理事件合成的。首先我們從上面我們知道,react并不是一次性把所有事件都綁定進(jìn)去,而是如果發(fā)現(xiàn)項(xiàng)目中有onClick,才綁定click事件,發(fā)現(xiàn)有onChange事件,才綁定blur , change , input , keydown , keyup等。所以為了把原理搞的清清楚楚,筆者把事件原理分成三部分來(lái)搞定:
1 react對(duì)事件是如何合成的。2 react事件是怎么綁定的。3 react事件觸發(fā)流程。
事件合成-事件插件
1 必要概念
我們先來(lái)看來(lái)幾個(gè)常量關(guān)系,這對(duì)于我們吃透react事件原理很有幫助。在解析來(lái)的講解中,我也會(huì)講到這幾個(gè)對(duì)象如何來(lái)的,具體有什么作用。
①namesToPlugins
第一個(gè)概念:namesToPlugins 裝事件名 -> 事件模塊插件的映射,namesToPlugins最終的樣子如下:
const namesToPlugins = {
SimpleEventPlugin,
EnterLeaveEventPlugin,
ChangeEventPlugin,
SelectEventPlugin,
BeforeInputEventPlugin,
}
SimpleEventPlugin等是處理各個(gè)事件函數(shù)的插件,比如一次點(diǎn)擊事件,就會(huì)找到SimpleEventPlugin對(duì)應(yīng)的處理函數(shù)。我們先記錄下它,至于具體有什么作用,接下來(lái)會(huì)講到。
②plugins
plugins,這個(gè)對(duì)象就是上面注冊(cè)的所有插件列表,初始化為空。
const plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];
③registrationNameModules
registrationNameModules記錄了React合成的事件-對(duì)應(yīng)的事件插件的關(guān)系,在React中,處理props中事件的時(shí)候,會(huì)根據(jù)不同的事件名稱,找到對(duì)應(yīng)的事件插件,然后統(tǒng)一綁定在document上。對(duì)于沒(méi)有出現(xiàn)過(guò)的事件,就不會(huì)綁定,我們接下來(lái)會(huì)講到。registrationNameModules大致的樣子如下所示。
{
onBlur: SimpleEventPlugin,
onClick: SimpleEventPlugin,
onClickCapture: SimpleEventPlugin,
onChange: ChangeEventPlugin,
onChangeCapture: ChangeEventPlugin,
onMouseEnter: EnterLeaveEventPlugin,
onMouseLeave: EnterLeaveEventPlugin,
...
}
④事件插件
那么我們首先就要搞清楚,SimpleEventPlugin,EnterLeaveEventPlugin每個(gè)插件都是什么?我們拿SimpleEventPlugin為例,看一下它究竟是什么樣子?
const SimpleEventPlugin = {
eventTypes:{
'click':{ /* 處理點(diǎn)擊事件 */
phasedRegistrationNames:{
bubbled: 'onClick', // 對(duì)應(yīng)的事件冒泡 - onClick
captured:'onClickCapture' //對(duì)應(yīng)事件捕獲階段 - onClickCapture
},
dependencies: ['click'], //事件依賴
...
},
'blur':{ /* 處理失去焦點(diǎn)事件 */ },
...
}
extractEvents:function(topLevelType,targetInst,){ /* eventTypes 里面的事件對(duì)應(yīng)的統(tǒng)一事件處理函數(shù),接下來(lái)會(huì)重點(diǎn)講到 */ }
}
首先事件插件是一個(gè)對(duì)象,有兩個(gè)屬性,第一個(gè)extractEvents作為事件統(tǒng)一處理函數(shù),第二個(gè)eventTypes是一個(gè)對(duì)象,對(duì)象保存了原生事件名和對(duì)應(yīng)的配置項(xiàng)dispatchConfig的映射關(guān)系。由于v16React的事件是統(tǒng)一綁定在document上的,React用獨(dú)特的事件名稱比如onClick和onClickCapture,來(lái)說(shuō)明我們給綁定的函數(shù)到底是在冒泡事件階段,還是捕獲事件階段執(zhí)行。
⑤ registrationNameDependencies
registrationNameDependencies用來(lái)記錄,合成事件比如 onClick 和原生事件 click對(duì)應(yīng)關(guān)系。比如 onChange 對(duì)應(yīng) change , input , keydown , keyup事件。
{
onBlur: ['blur'],
onClick: ['click'],
onClickCapture: ['click'],
onChange: ['blur', 'change', 'click', 'focus', 'input', 'keydown', 'keyup', 'selectionchange'],
onMouseEnter: ['mouseout', 'mouseover'],
onMouseLeave: ['mouseout', 'mouseover'],
...
}
2 事件初始化
對(duì)于事件合成,v16.13.1版本react采用了初始化注冊(cè)方式。
react-dom/src/client/ReactDOMClientInjection.js
/* 第一步:注冊(cè)事件: */
injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
injectEventPluginsByName 這個(gè)函數(shù)具體有什么用呢,它在react底層是默認(rèn)執(zhí)行的。我們來(lái)簡(jiǎn)化這個(gè)函數(shù),看它到底是干什么的。
legacy-event/EventPluginRegistry.js
/* 注冊(cè)事件插件 */
export function injectEventPluginsByName(injectedNamesToPlugins){
for (const pluginName in injectedNamesToPlugins) {
namesToPlugins[pluginName] = injectedNamesToPlugins[pluginName]
}
recomputePluginOrdering()
}
injectEventPluginsByName做的事情很簡(jiǎn)單,形成上述的namesToPlugins,然后執(zhí)行recomputePluginOrdering,我們接下來(lái)看一下recomputePluginOrdering做了寫(xiě)什么?
const eventPluginOrder = [ 'SimpleEventPlugin' , 'EnterLeaveEventPlugin','ChangeEventPlugin','SelectEventPlugin' , 'BeforeInputEventPlugin' ]
function recomputePluginOrdering(){
for (const pluginName in namesToPlugins) {
/* 找到對(duì)應(yīng)的事件處理插件,比如 SimpleEventPlugin */
const pluginModule = namesToPlugins[pluginName];
const pluginIndex = eventPluginOrder.indexOf(pluginName);
/* 填充 plugins 數(shù)組 */
plugins[pluginIndex] = pluginModule;
}
const publishedEvents = pluginModule.eventTypes;
for (const eventName in publishedEvents) {
// publishedEvents[eventName] -> eventConfig , pluginModule -> 事件插件 , eventName -> 事件名稱
publishEventForPlugin(publishedEvents[eventName],pluginModule,eventName,)
}
}
】
recomputePluginOrdering,作用很明確了,形成上面說(shuō)的那個(gè)plugins,數(shù)組。然后就是重點(diǎn)的函數(shù)publishEventForPlugin。
/*
dispatchConfig -> 原生事件對(duì)應(yīng)配置項(xiàng) { phasedRegistrationNames :{ 冒泡 捕獲 } , }
pluginModule -> 事件插件 比如SimpleEventPlugin
eventName -> 原生事件名稱。
*/
function publishEventForPlugin (dispatchConfig,pluginModule,eventName){
eventNameDispatchConfigs[eventName] = dispatchConfig;
/* 事件 */
const phasedRegistrationNames = dispatchConfig.phasedRegistrationNames;
if (phasedRegistrationNames) {
for (const phaseName in phasedRegistrationNames) {
if (phasedRegistrationNames.hasOwnProperty(phaseName)) {
// phasedRegistrationName React事件名 比如 onClick / onClickCapture
const phasedRegistrationName = phasedRegistrationNames[phaseName];
// 填充形成 registrationNameModules React 合成事件 -> React 處理事件插件映射關(guān)系
registrationNameModules[phasedRegistrationName] = pluginModule;
// 填充形成 registrationNameDependencies React 合成事件 -> 原生事件 映射關(guān)系
registrationNameDependencies[phasedRegistrationName] = pluginModule.eventTypes[eventName].dependencies;
}
}
return true;
}
}
publishEventForPlugin 作用形成上述的 registrationNameModules 和 registrationNameDependencies 對(duì)象中的映射關(guān)系。
3 事件合成總結(jié)
到這里整個(gè)初始化階段已經(jīng)完事了,我來(lái)總結(jié)一下初始化事件合成都做了些什么。這個(gè)階段主要形成了上述的幾個(gè)重要對(duì)象,構(gòu)建初始化React合成事件和原生事件的對(duì)應(yīng)關(guān)系,合成事件和對(duì)應(yīng)的事件處理插件關(guān)系。接下來(lái)就是事件綁定階段。
三 事件綁定-從一次點(diǎn)擊事件開(kāi)始
事件綁定流程
如果我們?cè)谝粋€(gè)組件中這么寫(xiě)一個(gè)點(diǎn)擊事件,React會(huì)一步步如何處理。
1 diffProperties 處理React合成事件
<div>
<button onClick={ this.handerClick } className="button" >點(diǎn)擊</button>
</div>
第一步,首先通過(guò)上面的講解,我們綁定給hostComponent種類(lèi)的fiber(如上的button元素),會(huì) button 對(duì)應(yīng)的fiber上,以memoizedProps 和 pendingProps形成保存。
button 對(duì)應(yīng) fiber
memoizedProps = {
onClick:function handerClick(){},
className:'button'
}
結(jié)構(gòu)圖如下所示:

第二步,React在調(diào)合子節(jié)點(diǎn)后,進(jìn)入diff階段,如果判斷是HostComponent(dom元素)類(lèi)型的fiber,會(huì)用diff props函數(shù)diffProperties單獨(dú)處理。
react-dom/src/client/ReactDOMComponent.js
function diffProperties(){
/* 判斷當(dāng)前的 propKey 是不是 React合成事件 */
if(registrationNameModules.hasOwnProperty(propKey)){
/* 這里多個(gè)函數(shù)簡(jiǎn)化了,如果是合成事件, 傳入成事件名稱 onClick ,向document注冊(cè)事件 */
legacyListenToEvent(registrationName, document);
}
}
diffProperties函數(shù)在 diff props 如果發(fā)現(xiàn)是合成事件(onClick) 就會(huì)調(diào)用legacyListenToEvent函數(shù)。注冊(cè)事件監(jiān)聽(tīng)器。
2 legacyListenToEvent 注冊(cè)事件監(jiān)聽(tīng)器
react-dom/src/events/DOMLegacyEventPluginSystem.js
// registrationName -> onClick 事件
// mountAt -> document or container
function legacyListenToEvent(registrationName,mountAt){
const dependencies = registrationNameDependencies[registrationName]; // 根據(jù) onClick 獲取 onClick 依賴的事件數(shù)組 [ 'click' ]。
for (let i = 0; i < dependencies.length; i++) {
const dependency = dependencies[i];
//這個(gè)經(jīng)過(guò)多個(gè)函數(shù)簡(jiǎn)化,如果是 click 基礎(chǔ)事件,會(huì)走 legacyTrapBubbledEvent ,而且都是按照冒泡處理
legacyTrapBubbledEvent(dependency, mountAt);
}
}
legacyTrapBubbledEvent 就是執(zhí)行將綁定真正的dom事件的函數(shù) legacyTrapBubbledEvent(冒泡處理)。
function legacyTrapBubbledEvent(topLevelType,element){
addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false)
}
第三步:在legacyListenToEvent函數(shù)中,先找到 React 合成事件對(duì)應(yīng)的原生事件集合,比如 onClick -> ['click'] , onChange -> [blur , change , input , keydown , keyup],然后遍歷依賴項(xiàng)的數(shù)組,綁定事件,這就解釋了,為什么我們?cè)趧傞_(kāi)始的demo中,只給元素綁定了一個(gè)onChange事件,結(jié)果在document上出現(xiàn)很多事件監(jiān)聽(tīng)器的原因,就是在這個(gè)函數(shù)上處理的。
我們上面已經(jīng)透露了React是采用事件綁定,React 對(duì)于 click 等基礎(chǔ)事件,會(huì)默認(rèn)按照事件冒泡階段的事件處理,不過(guò)這也不絕對(duì)的,比如一些事件的處理,有些特殊的事件是按照事件捕獲處理的。
case TOP_SCROLL: { // scroll 事件
legacyTrapCapturedEvent(TOP_SCROLL, mountAt); // legacyTrapCapturedEvent 事件捕獲處理。
break;
}
case TOP_FOCUS: // focus 事件
case TOP_BLUR: // blur 事件
legacyTrapCapturedEvent(TOP_FOCUS, mountAt);
legacyTrapCapturedEvent(TOP_BLUR, mountAt);
break;
3 綁定 dispatchEvent,進(jìn)行事件監(jiān)聽(tīng)
如上述的scroll事件,focus 事件 ,blur事件等,是默認(rèn)按照事件捕獲邏輯處理。接下來(lái)就是最重要關(guān)鍵的一步。React是如何綁定事件到document?事件處理函數(shù)函數(shù)又是什么?問(wèn)題都指向了上述的addTrappedEventListener,讓我們來(lái)揭開(kāi)它的面紗。
/*
targetContainer -> document
topLevelType -> click
capture = false
*/
function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer)
if(capture){
// 事件捕獲階段處理函數(shù)。
}else{
/* TODO: 重要, 這里進(jìn)行真正的事件綁定。*/
targetContainer.addEventListener(topLevelType,listener,false) // document.addEventListener('click',listener,false)
}
}
第四步:這個(gè)函數(shù)內(nèi)容雖然不多,但是卻非常重要,首先綁定我們的事件統(tǒng)一處理函數(shù) dispatchEvent,綁定幾個(gè)默認(rèn)參數(shù),事件類(lèi)型 topLevelType demo中的click ,還有綁定的容器doucment。然后真正的事件綁定,添加事件監(jiān)聽(tīng)器addEventListener。 事件綁定階段完畢。
4 事件綁定過(guò)程總結(jié)
我們來(lái)做一下事件綁定階段的總結(jié)。
① 在React,diff DOM元素類(lèi)型的fiber的props的時(shí)候, 如果發(fā)現(xiàn)是React合成事件,比如 onClick,會(huì)按照事件系統(tǒng)邏輯單獨(dú)處理。② 根據(jù)React合成事件類(lèi)型,找到對(duì)應(yīng)的原生事件的類(lèi)型,然后調(diào)用判斷原生事件類(lèi)型,大部分事件都按照冒泡邏輯處理,少數(shù)事件會(huì)按照捕獲邏輯處理(比如 scroll事件)。③ 調(diào)用 addTrappedEventListener 進(jìn)行真正的事件綁定,綁定在 document上,dispatchEvent為統(tǒng)一的事件處理函數(shù)。④ 有一點(diǎn)值得注意: 只有上述那幾個(gè)特殊事件比如 scorll,focus,blur等是在事件捕獲階段發(fā)生的,其他的都是在事件冒泡階段發(fā)生的,無(wú)論是onClick還是onClickCapture都是發(fā)生在冒泡階段,至于 React 本身怎么處理捕獲邏輯的。我們接下來(lái)會(huì)講到。
四 事件觸發(fā)-一次點(diǎn)擊事件,在react底層系統(tǒng)會(huì)發(fā)生什么?
<div>
<button onClick={ this.handerClick } className="button" >點(diǎn)擊</button>
</div>
還是上面這段代碼片段,當(dāng)點(diǎn)擊一下按鈕,在 React 底層會(huì)發(fā)生什么呢?接下來(lái),讓我共同探索事件觸發(fā)的奧秘。
事件觸發(fā)處理函數(shù) dispatchEvent
我們?cè)谑录壎A段講過(guò),React事件注冊(cè)時(shí)候,統(tǒng)一的監(jiān)聽(tīng)器dispatchEvent,也就是當(dāng)我們點(diǎn)擊按鈕之后,首先執(zhí)行的是dispatchEvent函數(shù),因?yàn)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">dispatchEvent前三個(gè)參數(shù)已經(jīng)被bind了進(jìn)去,所以真正的事件源對(duì)象event,被默認(rèn)綁定成第四個(gè)參數(shù)。
react-dom/src/events/ReactDOMEventListener.js
function dispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
/* 嘗試調(diào)度事件 */
const blockedOn = attemptToDispatchEvent( topLevelType,eventSystemFlags, targetContainer, nativeEvent);
}
/*
topLevelType -> click
eventSystemFlags -> 1
targetContainer -> document
nativeEvent -> 原生事件的 event 對(duì)象
*/
function attemptToDispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
/* 獲取原生事件 e.target */
const nativeEventTarget = getEventTarget(nativeEvent)
/* 獲取當(dāng)前事件,最近的dom類(lèi)型fiber ,我們 demo中 button 按鈕對(duì)應(yīng)的 fiber */
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
/* 重要:進(jìn)入legacy模式的事件處理系統(tǒng) */
dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst,);
return null;
}
在這個(gè)階段主要做了這幾件事:
① 首先根據(jù)真實(shí)的事件源對(duì)象,找到 e.target真實(shí)的dom元素。② 然后根據(jù) dom元素,找到與它對(duì)應(yīng)的fiber對(duì)象targetInst,在我們demo中,找到button按鈕對(duì)應(yīng)的fiber。③ 然后正式進(jìn)去 legacy模式的事件處理系統(tǒng),也就是我們目前用的React模式都是legacy模式下的,在這個(gè)模式下,批量更新原理,即將拉開(kāi)帷幕。
這里有一點(diǎn)問(wèn)題,React怎么樣通過(guò)原生的dom元素,找到對(duì)應(yīng)的fiber的呢? ,也就是說(shuō) getClosestInstanceFromNode 原理是什么?
答案是首先 getClosestInstanceFromNode 可以找到當(dāng)前傳入的 dom 對(duì)應(yīng)的最近的元素類(lèi)型的 fiber 對(duì)象。React 在初始化真實(shí) dom 的時(shí)候,用一個(gè)隨機(jī)的 key internalInstanceKey 指針指向了當(dāng)前dom對(duì)應(yīng)的fiber對(duì)象,fiber對(duì)象用stateNode指向了當(dāng)前的dom元素。
// 聲明隨機(jī)key
var internalInstanceKey = '__reactInternalInstance$' + randomKey;
// 使用隨機(jī)key
function getClosestInstanceFromNode(targetNode){
// targetNode -dom targetInst -> 與之對(duì)應(yīng)的fiber對(duì)象
var targetInst = targetNode[internalInstanceKey];
}
在谷歌調(diào)試器上看

兩者關(guān)系圖

legacy 事件處理系統(tǒng)與批量更新
react-dom/src/events/DOMLegacyEventPluginSystem.js
/* topLevelType - click事件 | eventSystemFlags = 1 | nativeEvent = 事件源對(duì)象 | targetInst = 元素對(duì)應(yīng)的fiber對(duì)象 */
function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
/* 從React 事件池中取出一個(gè),將 topLevelType ,targetInst 等屬性賦予給事件 */
const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
try { /* 執(zhí)行批量更新 handleTopLevel 為事件處理的主要函數(shù) */
batchedEventUpdates(handleTopLevel, bookKeeping);
} finally {
/* 釋放事件池 */
releaseTopLevelCallbackBookKeeping(bookKeeping);
}
}
對(duì)于v16事件池,我們接下來(lái)會(huì)講到,首先 batchedEventUpdates為批量更新的主要函數(shù)。我們先來(lái)看看batchedEventUpdates
react-dom/src/events/ReactDOMUpdateBatching.js
export function batchedEventUpdates(fn,a){
isBatchingEventUpdates = true;
try{
fn(a) // handleTopLevel(bookKeeping)
}finally{
isBatchingEventUpdates = false
}
}
批量更新簡(jiǎn)化成如上的樣子,從上面我們可以看到,React通過(guò)開(kāi)關(guān)isBatchingEventUpdates來(lái)控制是否啟用批量更新。fn(a),事件上調(diào)用的是 handleTopLevel(bookKeeping) ,由于js是單線程的,我們真正在組件中寫(xiě)的事件處理函數(shù),比如demo 的 handerClick實(shí)際執(zhí)行是在handleTopLevel(bookKeeping)中執(zhí)行的。所以如果我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">handerClick里面觸發(fā)setState,那么就能讀取到isBatchingEventUpdates = true這就是React的合成事件為什么具有批量更新的功能了。比如我們這么寫(xiě)
state={number:0}
handerClick = () =>{
this.setState({number: this.state.number + 1 })
console.log(this.state.number) //0
this.setState({number: this.state.number + 1 })
console.log(this.state.number) //0
setTimeout(()=>{
this.setState({number: this.state.number + 1 })
console.log(this.state.number) //2
this.setState({number: this.state.number + 1 })
console.log(this.state.number)// 3
})
}
如上述所示,第一個(gè)setState和第二個(gè)setState在批量更新條件之內(nèi)執(zhí)行,所以打印不會(huì)是最新的值,但是如果是發(fā)生在setTimeout中,由于eventLoop 放在了下一次事件循環(huán)中執(zhí)行,此時(shí) batchedEventUpdates 中已經(jīng)執(zhí)行完isBatchingEventUpdates = false,所以批量更新被打破,我們就可以直接訪問(wèn)到最新變化的值了。
接下來(lái)我們有兩點(diǎn)沒(méi)有梳理:
一是React事件池概念 二是最后的線索是執(zhí)行 handleTopLevel(bookKeeping),那么handleTopLevel到底做了寫(xiě)什么。
執(zhí)行事件插件函數(shù)
上面說(shuō)到整個(gè)事件系統(tǒng),最后指向函數(shù) handleTopLevel(bookKeeping) 那么 handleTopLevel 到底做了什么事情?
// 流程簡(jiǎn)化后
// topLevelType - click
// targetInst - button Fiber
// nativeEvent
function handleTopLevel(bookKeeping){
const { topLevelType,targetInst,nativeEvent,eventTarget, eventSystemFlags} = bookKeeping
for(let i=0; i < plugins.length;i++ ){
const possiblePlugin = plugins[i];
/* 找到對(duì)應(yīng)的事件插件,形成對(duì)應(yīng)的合成event,形成事件執(zhí)行隊(duì)列 */
const extractedEvents = possiblePlugin.extractEvents(topLevelType,targetInst,nativeEvent,eventTarget,eventSystemFlags)
}
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
/* 執(zhí)行事件處理函數(shù) */
runEventsInBatch(events);
}
我把整個(gè)流程簡(jiǎn)化,只保留了核心的流程,handleTopLevel最后的處理邏輯就是執(zhí)行我們說(shuō)的事件處理插件(SimpleEventPlugin)中的處理函數(shù)extractEvents,比如我們demo中的點(diǎn)擊事件 onClick 最終走的就是 SimpleEventPlugin 中的 extractEvents 函數(shù),那么React為什么這么做呢? 我們知道我們React是采取事件合成,事件統(tǒng)一綁定,并且我們寫(xiě)在組件中的事件處理函數(shù)( handerClick ),也不是真正的執(zhí)行函數(shù)dispatchAciton,那么我們?cè)?code style="font-size: 14px;overflow-wrap: break-word;padding: 2px 4px;border-radius: 4px;margin-right: 2px;margin-left: 2px;background-color: rgba(27, 31, 35, 0.05);font-family: "Operator Mono", Consolas, Monaco, Menlo, monospace;word-break: break-all;color: rgb(239, 112, 96);">handerClick的事件對(duì)象 event,也是React單獨(dú)合成處理的,里面單獨(dú)封裝了比如 stopPropagation和preventDefault等方法,這樣的好處是,我們不需要跨瀏覽器單獨(dú)處理兼容問(wèn)題,交給React底層統(tǒng)一處理。
extractEvents 形成事件對(duì)象event 和 事件處理函數(shù)隊(duì)列
重點(diǎn)來(lái)了!重點(diǎn)來(lái)了!重點(diǎn)來(lái)了!,extractEvents 可以作為整個(gè)事件系統(tǒng)核心函數(shù),我們先回到最初的demo,如果我們這么寫(xiě),那么四個(gè)回調(diào)函數(shù),那么點(diǎn)擊按鈕,四個(gè)事件是如何處理的呢。首先如果點(diǎn)擊按鈕,最終走的就是extractEvents函數(shù),一探究竟這個(gè)函數(shù)。
legacy-events/SyntheticEvent.js
const SimpleEventPlugin = {
extractEvents:function(topLevelType,targetInst,nativeEvent,nativeEventTarget){
const dispatchConfig = topLevelEventsToDispatchConfig.get(topLevelType);
if (!dispatchConfig) {
return null;
}
switch(topLevelType){
default:
EventConstructor = SyntheticEvent;
break;
}
/* 產(chǎn)生事件源對(duì)象 */
const event = EventConstructor.getPooled(dispatchConfig,targetInst,nativeEvent,nativeEventTarget)
const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
const dispatchListeners = [];
const {bubbled, captured} = phasedRegistrationNames; /* onClick / onClickCapture */
const dispatchInstances = [];
/* 從事件源開(kāi)始逐漸向上,查找dom元素類(lèi)型HostComponent對(duì)應(yīng)的fiber ,收集上面的React合成事件,onClick / onClickCapture */
while (instance !== null) {
const {stateNode, tag} = instance;
if (tag === HostComponent && stateNode !== null) { /* DOM 元素 */
const currentTarget = stateNode;
if (captured !== null) { /* 事件捕獲 */
/* 在事件捕獲階段,真正的事件處理函數(shù) */
const captureListener = getListener(instance, captured);
if (captureListener != null) {
/* 對(duì)應(yīng)發(fā)生在事件捕獲階段的處理函數(shù),邏輯是將執(zhí)行函數(shù)unshift添加到隊(duì)列的最前面 */
dispatchListeners.unshift(captureListener);
dispatchInstances.unshift(instance);
dispatchCurrentTargets.unshift(currentTarget);
}
}
if (bubbled !== null) { /* 事件冒泡 */
/* 事件冒泡階段,真正的事件處理函數(shù),邏輯是將執(zhí)行函數(shù)push到執(zhí)行隊(duì)列的最后面 */
const bubbleListener = getListener(instance, bubbled);
if (bubbleListener != null) {
dispatchListeners.push(bubbleListener);
dispatchInstances.push(instance);
dispatchCurrentTargets.push(currentTarget);
}
}
}
instance = instance.return;
}
if (dispatchListeners.length > 0) {
/* 將函數(shù)執(zhí)行隊(duì)列,掛到事件對(duì)象event上 */
event._dispatchListeners = dispatchListeners;
event._dispatchInstances = dispatchInstances;
event._dispatchCurrentTargets = dispatchCurrentTargets;
}
return event
}
}
事件插件系統(tǒng)的核心extractEvents主要做的事是:
① 首先形成 React事件獨(dú)有的合成事件源對(duì)象,這個(gè)對(duì)象,保存了整個(gè)事件的信息。將作為參數(shù)傳遞給真正的事件處理函數(shù)(handerClick)。② 然后聲明事件執(zhí)行隊(duì)列 ,按照 冒泡和捕獲邏輯,從事件源開(kāi)始逐漸向上,查找dom元素類(lèi)型HostComponent對(duì)應(yīng)的fiber ,收集上面的React合成事件,例如onClick/onClickCapture,對(duì)于冒泡階段的事件(onClick),將push到執(zhí)行隊(duì)列后面 , 對(duì)于捕獲階段的事件(onClickCapture),將unShift到執(zhí)行隊(duì)列的前面。③ 最后將事件執(zhí)行隊(duì)列,保存到React事件源對(duì)象上。等待執(zhí)行。
舉個(gè)例子比如如下
handerClick = () => console.log(1)
handerClick1 = () => console.log(2)
handerClick2 = () => console.log(3)
handerClick3= () => console.log(4)
render(){
return <div onClick={ this.handerClick2 } onClickCapture={this.handerClick3} >
<button onClick={ this.handerClick } onClickCapture={ this.handerClick1 } className="button" >點(diǎn)擊</button>
</div>
}
打印 // 4 2 1 3
看到這里我們應(yīng)該知道上述函數(shù)打印順序?yàn)槭裁戳税?,首先遍歷 button 對(duì)應(yīng)的fiber,首先遇到了 onClickCapture ,將 handerClick1 放到了數(shù)組最前面,然后又把onClick對(duì)應(yīng)handerClick的放到數(shù)組的最后面,形成的結(jié)構(gòu)是[ handerClick1 , handerClick ] , 然后向上遍歷,遇到了div對(duì)應(yīng)fiber,將onClickCapture對(duì)應(yīng)的handerClick3放在了數(shù)組前面,將onClick對(duì)應(yīng)的 handerClick2 放在了數(shù)組后面,形成的結(jié)構(gòu) [ handerClick3,handerClick1 , handerClick,handerClick2 ] ,所以執(zhí)行的順序 // 4 2 1 3,就是這么簡(jiǎn)單,完美!

事件觸發(fā)
有的同學(xué)可能好奇React的事件源對(duì)象是什么樣的,以上面代碼中SyntheticEvent為例子我們一起來(lái)看看:
legacy-events/SyntheticEvent.js/
function SyntheticEvent( dispatchConfig,targetInst,nativeEvent,nativeEventTarget){
this.dispatchConfig = dispatchConfig;
this._targetInst = targetInst;
this.nativeEvent = nativeEvent;
this._dispatchListeners = null;
this._dispatchInstances = null;
this._dispatchCurrentTargets = null;
this.isPropagationStopped = () => false; /* 初始化,返回為false */
}
SyntheticEvent.prototype={
stopPropagation(){ this.isPropagationStopped = () => true; }, /* React單獨(dú)處理,阻止事件冒泡函數(shù) */
preventDefault(){ }, /* React單獨(dú)處理,阻止事件捕獲函數(shù) */
...
}
在 handerClick 中打印 e :

既然事件執(zhí)行隊(duì)列和事件源對(duì)象都形成了,接下來(lái)就是最后一步事件觸發(fā)了。上面大家有沒(méi)有注意到一個(gè)函數(shù)runEventsInBatch,所有事件綁定函數(shù),就是在這里觸發(fā)的。讓我們一起看看。
legacy-events/EventBatching.js
function runEventsInBatch(){
const dispatchListeners = event._dispatchListeners;
const dispatchInstances = event._dispatchInstances;
if (Array.isArray(dispatchListeners)) {
for (let i = 0; i < dispatchListeners.length; i++) {
if (event.isPropagationStopped()) { /* 判斷是否已經(jīng)阻止事件冒泡 */
break;
}
dispatchListeners[i](event)
}
}
/* 執(zhí)行完函數(shù),置空兩字段 */
event._dispatchListeners = null;
event._dispatchInstances = null;
}
dispatchListeners[i](event)就是執(zhí)行我們的事件處理函數(shù)比如handerClick,從這里我們知道,我們?cè)谑录幚砗瘮?shù)中,返回 false ,并不會(huì)阻止瀏覽器默認(rèn)行為。
handerClick(){ //并不能阻止瀏覽器默認(rèn)行為。
return false
}
應(yīng)該改成這樣:
handerClick(e){
e.preventDefault()
}
另一方面React對(duì)于阻止冒泡,就是通過(guò)isPropagationStopped,判斷是否已經(jīng)阻止事件冒泡。如果我們?cè)谑录瘮?shù)執(zhí)行隊(duì)列中,某一會(huì)函數(shù)中,調(diào)用e.stopPropagation(),就會(huì)賦值給isPropagationStopped=()=>true,當(dāng)再執(zhí)行 e.isPropagationStopped()就會(huì)返回 true ,接下來(lái)事件處理函數(shù),就不會(huì)執(zhí)行了。
其他概念-事件池
handerClick = (e) => {
console.log(e.target) // button
setTimeout(()=>{
console.log(e.target) // null
},0)
}
對(duì)于一次點(diǎn)擊事件的處理函數(shù),在正常的函數(shù)執(zhí)行上下文中打印e.target就指向了dom元素,但是在setTimeout中打印卻是null,如果這不是React事件系統(tǒng),兩次打印的應(yīng)該是一樣的,但是為什么兩次打印不一樣呢?因?yàn)樵赗eact采取了一個(gè)事件池的概念,每次我們用的事件源對(duì)象,在事件函數(shù)執(zhí)行之后,可以通過(guò)releaseTopLevelCallbackBookKeeping等方法將事件源對(duì)象釋放到事件池中,這樣的好處每次我們不必再創(chuàng)建事件源對(duì)象,可以從事件池中取出一個(gè)事件源對(duì)象進(jìn)行復(fù)用,在事件處理函數(shù)執(zhí)行完畢后,會(huì)釋放事件源到事件池中,清空屬性,這就是setTimeout中打印為什么是null的原因了。
事件觸發(fā)總結(jié)
我把事件觸發(fā)階段做的事總結(jié)一下:
①首先通過(guò)統(tǒng)一的事件處理函數(shù)
dispatchEvent,進(jìn)行批量更新batchUpdate。②然后執(zhí)行事件對(duì)應(yīng)的處理插件中的
extractEvents,合成事件源對(duì)象,每次React會(huì)從事件源開(kāi)始,從上遍歷類(lèi)型為 hostComponent即 dom類(lèi)型的fiber,判斷props中是否有當(dāng)前事件比如onClick,最終形成一個(gè)事件執(zhí)行隊(duì)列,React就是用這個(gè)隊(duì)列,來(lái)模擬事件捕獲->事件源->事件冒泡這一過(guò)程。③最后通過(guò)
runEventsInBatch執(zhí)行事件隊(duì)列,如果發(fā)現(xiàn)阻止冒泡,那么break跳出循環(huán),最后重置事件源,放回到事件池中,完成整個(gè)流程。

五 關(guān)于react v17版本的事件系統(tǒng)
React v17 整體改動(dòng)不是很大,但是事件系統(tǒng)的改動(dòng)卻不小,首先上述的很多執(zhí)行函數(shù),在v17版本不復(fù)存在了。我來(lái)簡(jiǎn)單描述一下v17事件系統(tǒng)的改版。
1 事件統(tǒng)一綁定container上,ReactDOM.render(app, container);而不是document上,這樣好處是有利于微前端的,微前端一個(gè)前端系統(tǒng)中可能有多個(gè)應(yīng)用,如果繼續(xù)采取全部綁定在document上,那么可能多應(yīng)用下會(huì)出現(xiàn)問(wèn)題。

2 對(duì)齊原生瀏覽器事件
React 17中終于支持了原生捕獲事件的支持, 對(duì)齊了瀏覽器原生標(biāo)準(zhǔn)。同時(shí) onScroll 事件不再進(jìn)行事件冒泡。onFocus 和 onBlur 使用原生 focusin, focusout 合成。
3 取消事件池React 17取消事件池復(fù)用,也就解決了上述在setTimeout打印,找不到e.target的問(wèn)題。
六 總結(jié)
本文從事件合成,事件綁定,事件觸發(fā)三個(gè)方面詳細(xì)介紹了React事件系統(tǒng)原理,希望大家能通過(guò)這篇文章更加深入了解v16 React 事件系統(tǒng),如果有疑問(wèn)和不足之處,也希望大家能在評(píng)論區(qū)指出。
最后, 送人玫瑰,手留余香,覺(jué)得有收獲的朋友可以給筆者點(diǎn)贊,關(guān)注一波 ,陸續(xù)更新前端超硬核文章。
提前透漏:接下來(lái)會(huì)出一部揭秘react調(diào)度系統(tǒng)的文章。感興趣的同學(xué)請(qǐng)關(guān)注公眾號(hào) 前端Sharing 第一時(shí)間更新前端硬文。
參考文檔
react源碼
React 事件系統(tǒng)工作原理
