「React 進階」一文喫透 react 事件原理

一 前言

=======

今天我們來一起探討一下React事件原理,這篇文章,我儘量用通俗簡潔的方式,把React事件系統講的明明白白。

我們講的react版本是16.13.1 , v17之後react對於事件系統會有相關的改版,文章後半部分會提及。

老規矩,在正式講解react之前,我們先想想這幾個問題 (如果我是面試官,你會怎麼回答?):

必要的知識概念

在弄清楚react事件之前,有幾個概念我們必須弄清楚,因爲只有弄明白這幾個概念,在事件觸發階段,我們才能更好的理解react處理事件本質。

我們寫在 JSX 事件終將變成什麼?

我們先寫一段含有點擊事件的react JSX語法,看一下它最終會變成什麼樣子?

class Index extends React.Component{
    handerClick= (value) => console.log(value) 
    render(){
        return <div>
            <button onClick={ this.handerClick } > 按鈕點擊 </button>
        </div>
    }
}

經過babel轉換成React.createElement形式,如下:

最終轉成fiber對象形式如下:

fiber對象上的memoizedPropspendingProps保存了我們的事件。

什麼是合成事件?

通過上一步我們看到了,我們聲明事件保存的位置。但是事件有沒有被真正的註冊呢?我們接下來看一下:

我們看一下當前這個元素<button>上有沒有綁定這個事件監聽器呢?

button 上綁定的事件

我們可以看到 ,button上綁定了兩個事件,一個是document上的事件監聽器,另外一個是button,但是事件處理函數handle,並不是我們的handerClick事件,而是noop

noop是什麼呢?我們接着來看。

原來noop就指向一個空函數。

然後我們看document綁定的事件

可以看到click事件被綁定在document上了。

接下來我們再搞搞事情😂😂😂,在demo項目中加上一個input輸入框,並綁定一個onChange事件。睜大眼睛看看接下來會發生什麼?

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 } > 按鈕點擊 </button>
            <input  placeholder="請輸入內容" onChange={ this.handerChange }  />
        </div>
    }
}

我們先看一下input dom元素上綁定的事件

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

我們發現,我們給<input>綁定的onChange,並沒有直接綁定在input上,而是統一綁定在了document上,然後我們onChange被處理成很多事件監聽器,比如blur , change , input , keydown , keyup 等。

綜上我們可以得出結論:

那麼什麼是react事件合成呢?

react中,我們綁定的事件onClick等,並不是原生事件,而是由原生事件合成的React事件,比如 click事件合成爲onClick事件。比如blur , change , input , keydown , keyup等 , 合成爲onChange

那麼react採取這種事件合成的模式呢?

一方面,將事件綁定在document統一管理,防止很多事件直接綁定在原生的dom元素上。造成一些不可控的情況

另一方面, React 想實現一個全瀏覽器的框架, 爲了實現這種目標就需要提供全瀏覽器一致性的事件系統,以此抹平不同瀏覽器的差異。

接下來的文章中,會介紹react是怎麼做事件合成的。

dom 元素對應的 fiber Tag 對象

我們知道了react怎麼儲存了我們的事件函數和事件合成因果。接下來我想讓大家記住一種類型的 fiber 對象, 因爲後面會用到,這對後續的理解很有幫助。

我們先來看一個代碼片段:

<div> 
  <div> hello , my name is alien </div>
</div>

<div> hello , my name is alien </div> 對應的 fiber類型。tag = 5

然後我們去react源碼中找到這種類的fiber類型。

/react-reconciler/src/ReactWorkTagsq.js

export const HostComponent = 5; // 元素節點

好的 ,我們暫且把 HostComponentHostText記錄📝下來。接下來回歸正題,我們先來看看react事件合成機制。

二 事件初始化 - 事件合成,插件機制

接下來,我們來看一看react這麼處理事件合成的。首先我們從上面我們知道,react並不是一次性把所有事件都綁定進去,而是如果發現項目中有onClick,才綁定click事件,發現有onChange事件,才綁定blur , change , input , keydown , keyup等。所以爲了把原理搞的清清楚楚,筆者把事件原理分成三部分來搞定:

事件合成 - 事件插件

1 必要概念

我們先來看來幾個常量關係,這對於我們喫透react事件原理很有幫助。在解析來的講解中,我也會講到這幾個對象如何來的,具體有什麼作用。

①namesToPlugins

第一個概念:namesToPlugins 裝事件名 -> 事件模塊插件的映射,namesToPlugins最終的樣子如下:

const namesToPlugins = {
    SimpleEventPlugin,
    EnterLeaveEventPlugin,
    ChangeEventPlugin,
    SelectEventPlugin,
    BeforeInputEventPlugin,
}

SimpleEventPlugin等是處理各個事件函數的插件,比如一次點擊事件,就會找到SimpleEventPlugin對應的處理函數。我們先記錄下它,至於具體有什麼作用,接下來會講到。

②plugins

plugins,這個對象就是上面註冊的所有插件列表, 初始化爲空。

const  plugins = [LegacySimpleEventPlugin, LegacyEnterLeaveEventPlugin, ...];

③registrationNameModules

registrationNameModules記錄了 React 合成的事件 - 對應的事件插件的關係,在React中,處理props中事件的時候,會根據不同的事件名稱,找到對應的事件插件,然後統一綁定在document上。對於沒有出現過的事件,就不會綁定,我們接下來會講到。registrationNameModules大致的樣子如下所示。

{
    onBlur: SimpleEventPlugin,
    onClick: SimpleEventPlugin,
    onClickCapture: SimpleEventPlugin,
    onChange: ChangeEventPlugin,
    onChangeCapture: ChangeEventPlugin,
    onMouseEnter: EnterLeaveEventPlugin,
    onMouseLeave: EnterLeaveEventPlugin,
    ...
}

④事件插件

那麼我們首先就要搞清楚,SimpleEventPlugin,EnterLeaveEventPlugin每個插件都是什麼?我們拿SimpleEventPlugin爲例,看一下它究竟是什麼樣子?

const SimpleEventPlugin = {
    eventTypes:{ 
        'click':{ /* 處理點擊事件  */
            phasedRegistrationNames:{
                bubbled: 'onClick',       // 對應的事件冒泡 - onClick 
                captured:'onClickCapture' //對應事件捕獲階段 - onClickCapture
            },
            dependencies: ['click'], //事件依賴
            ...
        },
        'blur':{ /* 處理失去焦點事件 */ },
        ...
    }
    extractEvents:function(topLevelType,targetInst,){ /* eventTypes 裏面的事件對應的統一事件處理函數,接下來會重點講到 */ }
}

首先事件插件是一個對象,有兩個屬性,第一個extractEvents作爲事件統一處理函數,第二個eventTypes是一個對象,對象保存了原生事件名和對應的配置項dispatchConfig的映射關係。由於 v16React 的事件是統一綁定在document上的,React 用獨特的事件名稱比如onClickonClickCapture,來說明我們給綁定的函數到底是在冒泡事件階段,還是捕獲事件階段執行。

⑤ registrationNameDependencies

registrationNameDependencies用來記錄,合成事件比如 onClick 和原生事件 click對應關係。比如 onChange 對應 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 事件初始化

對於事件合成,v16.13.1版本react採用了初始化註冊方式。

react-dom/src/client/ReactDOMClientInjection.js

/* 第一步:註冊事件:  */
injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin,
});

injectEventPluginsByName 這個函數具體有什麼用呢,它在react底層是默認執行的。我們來簡化這個函數, 看它到底是幹什麼的。

legacy-event/EventPluginRegistry.js

/* 註冊事件插件 */
export function injectEventPluginsByName(injectedNamesToPlugins){
     for (const pluginName in injectedNamesToPlugins) {
         namesToPlugins[pluginName] = injectedNamesToPlugins[pluginName]
     }
     recomputePluginOrdering()
}

injectEventPluginsByName做的事情很簡單,形成上述的namesToPlugins,然後執行recomputePluginOrdering,我們接下來看一下recomputePluginOrdering做了寫什麼?

const eventPluginOrder = [ 'SimpleEventPlugin' , 'EnterLeaveEventPlugin','ChangeEventPlugin','SelectEventPlugin' , 'BeforeInputEventPlugin' ]

function recomputePluginOrdering(){
    for (const pluginName in namesToPlugins) {
        /* 找到對應的事件處理插件,比如 SimpleEventPlugin  */
        const pluginModule = namesToPlugins[pluginName];
        const pluginIndex = eventPluginOrder.indexOf(pluginName);
        /* 填充 plugins 數組  */
        plugins[pluginIndex] = pluginModule;
    }

    const publishedEvents = pluginModule.eventTypes;
    for (const eventName in publishedEvents) {
       // publishedEvents[eventName] -> eventConfig , pluginModule -> 事件插件 , eventName -> 事件名稱
        publishEventForPlugin(publishedEvents[eventName],pluginModule,eventName,)
    } 
}

recomputePluginOrdering, 作用很明確了,形成上面說的那個plugins, 數組。然後就是重點的函數publishEventForPlugin

/*
  dispatchConfig -> 原生事件對應配置項 { 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 處理事件插件映射關係
            registrationNameModules[phasedRegistrationName] = pluginModule;
            // 填充形成 registrationNameDependencies React 合成事件 -> 原生事件 映射關係
            registrationNameDependencies[phasedRegistrationName] = pluginModule.eventTypes[eventName].dependencies;
        }
    }
    return true;
    }
}

publishEventForPlugin 作用形成上述的 registrationNameModulesregistrationNameDependencies 對象中的映射關係。

3 事件合成總結

到這裏整個初始化階段已經完事了,我來總結一下初始化事件合成都做了些什麼。這個階段主要形成了上述的幾個重要對象,構建初始化 React 合成事件和原生事件的對應關係,合成事件和對應的事件處理插件關係。接下來就是事件綁定階段。

三 事件綁定 - 從一次點擊事件開始

事件綁定流程

如果我們在一個組件中這麼寫一個點擊事件,React會一步步如何處理。

1 diffProperties 處理 React 合成事件

<div>
  <button onClick={ this.handerClick }  class >點擊</button>
</div>

第一步,首先通過上面的講解,我們綁定給 hostComponent 種類的 fiber(如上的 button 元素),會 button 對應的 fiber 上,以memoizedPropspendingProps形成保存。

button 對應 fiber
memoizedProps = {
   onClick:function handerClick(){},
   className:'button'
}

結構圖如下所示:

第二步,React 在調合子節點後,進入 diff 階段,如果判斷是HostComponent(dom 元素) 類型的 fiber,會用 diff props 函數diffProperties單獨處理。

react-dom/src/client/ReactDOMComponent.js

function diffProperties(){
    /* 判斷當前的 propKey 是不是 React合成事件 */
    if(registrationNameModules.hasOwnProperty(propKey)){
         /* 這裏多個函數簡化了,如果是合成事件, 傳入成事件名稱 onClick ,向document註冊事件  */
         legacyListenToEvent(registrationName, document);
    }
}

diffProperties函數在 diff props 如果發現是合成事件 (onClick) 就會調用legacyListenToEvent函數。註冊事件監聽器。

2 legacyListenToEvent 註冊事件監聽器

react-dom/src/events/DOMLegacyEventPluginSystem.js

//  registrationName -> onClick 事件
//  mountAt -> document or container
function legacyListenToEvent(registrationName,mountAt){
   const dependencies = registrationNameDependencies[registrationName]; // 根據 onClick 獲取  onClick 依賴的事件數組 [ 'click' ]。
    for (let i = 0; i < dependencies.length; i++) {
    const dependency = dependencies[i];
    //這個經過多個函數簡化,如果是 click 基礎事件,會走 legacyTrapBubbledEvent ,而且都是按照冒泡處理
     legacyTrapBubbledEvent(dependency, mountAt);
  }
}

legacyTrapBubbledEvent 就是執行將綁定真正的 dom 事件的函數 legacyTrapBubbledEvent(冒泡處理)。

function legacyTrapBubbledEvent(topLevelType,element){
   addTrappedEventListener(element,topLevelType,PLUGIN_EVENT_SYSTEM,false)
}

第三步:在legacyListenToEvent函數中,先找到 React 合成事件對應的原生事件集合,比如 onClick -> ['click'] , onChange -> [blur , change , input , keydown , keyup],然後遍歷依賴項的數組,綁定事件,這就解釋了,爲什麼我們在剛開始的 demo 中,只給元素綁定了一個onChange事件,結果在document上出現很多事件監聽器的原因,就是在這個函數上處理的。

我們上面已經透露了 React 是採用事件綁定,React 對於 click 等基礎事件,會默認按照事件冒泡階段的事件處理,不過這也不絕對的,比如一些事件的處理,有些特殊的事件是按照事件捕獲處理的。

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,進行事件監聽

如上述的scroll事件,focus 事件 ,blur事件等,是默認按照事件捕獲邏輯處理。接下來就是最重要關鍵的一步。React 是如何綁定事件到document?事件處理函數函數又是什麼?問題都指向了上述的addTrappedEventListener,讓我們來揭開它的面紗。

/*
  targetContainer -> document
  topLevelType ->  click
  capture = false
*/
function addTrappedEventListener(targetContainer,topLevelType,eventSystemFlags,capture){
   const listener = dispatchEvent.bind(null,topLevelType,eventSystemFlags,targetContainer) 
   if(capture){
       // 事件捕獲階段處理函數。
   }else{
       /* TODO: 重要, 這裏進行真正的事件綁定。*/
      targetContainer.addEventListener(topLevelType,listener,false) // document.addEventListener('click',listener,false)
   }
}

第四步:這個函數內容雖然不多,但是卻非常重要, 首先綁定我們的事件統一處理函數 dispatchEvent,綁定幾個默認參數,事件類型 topLevelType demo 中的click ,還有綁定的容器doucment然後真正的事件綁定, 添加事件監聽器addEventListener 事件綁定階段完畢。

4 事件綁定過程總結

我們來做一下事件綁定階段的總結。

四 事件觸發 - 一次點擊事件,在react底層系統會發生什麼?

<div>
  <button onClick={ this.handerClick }  class >點擊</button>
</div>

還是上面這段代碼片段,當點擊一下按鈕,在 React 底層會發生什麼呢?接下來,讓我共同探索事件觸發的奧祕。

事件觸發處理函數 dispatchEvent

我們在事件綁定階段講過,React 事件註冊時候,統一的監聽器dispatchEvent,也就是當我們點擊按鈕之後,首先執行的是dispatchEvent函數,因爲dispatchEvent前三個參數已經被 bind 了進去,所以真正的事件源對象event,被默認綁定成第四個參數。

react-dom/src/events/ReactDOMEventListener.js

function dispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
    /* 嘗試調度事件 */
    const blockedOn = attemptToDispatchEvent( topLevelType,eventSystemFlags, targetContainer, nativeEvent);
}
/*
topLevelType -> click
eventSystemFlags -> 1
targetContainer -> document
nativeEvent -> 原生事件的 event 對象
*/
function attemptToDispatchEvent(topLevelType,eventSystemFlags,targetContainer,nativeEvent){
    /* 獲取原生事件 e.target */
    const nativeEventTarget = getEventTarget(nativeEvent)
    /* 獲取當前事件,最近的dom類型fiber ,我們 demo中 button 按鈕對應的 fiber */
    let targetInst = getClosestInstanceFromNode(nativeEventTarget); 
    /* 重要:進入legacy模式的事件處理系統 */
    dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst,);
    return null;
}

在這個階段主要做了這幾件事:

這裏有一點問題,React怎麼樣通過原生的dom元素,找到對應的fiber的呢? ,也就是說 getClosestInstanceFromNode 原理是什麼?

答案是首先 getClosestInstanceFromNode 可以找到當前傳入的 dom 對應的最近的元素類型的 fiber 對象。React 在初始化真實 dom 的時候,用一個隨機的 key internalInstanceKey  指針指向了當前dom對應的fiber對象,fiber對象用stateNode指向了當前的dom元素。

// 聲明隨機key
var internalInstanceKey = '__reactInternalInstance$' + randomKey;

// 使用隨機key 
function getClosestInstanceFromNode(targetNode){
  // targetNode -dom  targetInst -> 與之對應的fiber對象
  var targetInst = targetNode[internalInstanceKey];
}

在谷歌調試器上看

兩者關係圖

legacy 事件處理系統與批量更新

react-dom/src/events/DOMLegacyEventPluginSystem.js

/* topLevelType - click事件 | eventSystemFlags = 1 | nativeEvent = 事件源對象  | targetInst = 元素對應的fiber對象  */
function dispatchEventForLegacyPluginEventSystem(topLevelType,eventSystemFlags,nativeEvent,targetInst){
    /* 從React 事件池中取出一個,將 topLevelType ,targetInst 等屬性賦予給事件  */
    const bookKeeping = getTopLevelCallbackBookKeeping(topLevelType,nativeEvent,targetInst,eventSystemFlags);
    try { /* 執行批量更新 handleTopLevel 爲事件處理的主要函數 */
    batchedEventUpdates(handleTopLevel, bookKeeping);
  } finally {
    /* 釋放事件池 */  
    releaseTopLevelCallbackBookKeeping(bookKeeping);
  }
}

對於 v16 事件池,我們接下來會講到,首先 batchedEventUpdates爲批量更新的主要函數。我們先來看看batchedEventUpdates

react-dom/src/events/ReactDOMUpdateBatching.js

export function batchedEventUpdates(fn,a){
    isBatchingEventUpdates = true;
    try{
       fn(a) // handleTopLevel(bookKeeping)
    }finally{
        isBatchingEventUpdates = false
    }
}

批量更新簡化成如上的樣子,從上面我們可以看到,React 通過開關isBatchingEventUpdates來控制是否啓用批量更新。fn(a),事件上調用的是 handleTopLevel(bookKeeping) ,由於 js 是單線程的,我們真正在組件中寫的事件處理函數,比如 demo 的 handerClick實際執行是在handleTopLevel(bookKeeping)中執行的。所以如果我們在handerClick裏面觸發setState,那麼就能讀取到isBatchingEventUpdates = true這就是 React 的合成事件爲什麼具有批量更新的功能了。比如我們這麼寫

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
    })
}

如上述所示,第一個setState和第二個setState在批量更新條件之內執行,所以打印不會是最新的值,但是如果是發生在setTimeout中, 由於 eventLoop 放在了下一次事件循環中執行,此時 batchedEventUpdates 中已經執行完isBatchingEventUpdates = false,所以批量更新被打破,我們就可以直接訪問到最新變化的值了。

接下來我們有兩點沒有梳理:

執行事件插件函數

上面說到整個事件系統,最後指向函數 handleTopLevel(bookKeeping) 那麼 handleTopLevel 到底做了什麼事情?

// 流程簡化後
// 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];
        /* 找到對應的事件插件,形成對應的合成event,形成事件執行隊列  */
        const  extractedEvents = possiblePlugin.extractEvents(topLevelType,targetInst,nativeEvent,eventTarget,eventSystemFlags)  
    }
    if (extractedEvents) {
        events = accumulateInto(events, extractedEvents);
    }
    /* 執行事件處理函數 */
    runEventsInBatch(events);
}

我把整個流程簡化,只保留了核心的流程,handleTopLevel最後的處理邏輯就是執行我們說的事件處理插件 (SimpleEventPlugin) 中的處理函數extractEvents,比如我們 demo 中的點擊事件 onClick 最終走的就是 SimpleEventPlugin 中的 extractEvents 函數,那麼 React 爲什麼這麼做呢? 我們知道我們 React 是採取事件合成,事件統一綁定,並且我們寫在組件中的事件處理函數 (handerClick),也不是真正的執行函數dispatchAciton,那麼我們在handerClick的事件對象 event, 也是 React 單獨合成處理的,裏面單獨封裝了比如 stopPropagationpreventDefault等方法,這樣的好處是,我們不需要跨瀏覽器單獨處理兼容問題,交給 React 底層統一處理。

extractEvents 形成事件對象 event 和 事件處理函數隊列

重點來了!重點來了!重點來了!,extractEvents 可以作爲整個事件系統核心函數,我們先回到最初的demo,如果我們這麼寫, 那麼四個回調函數,那麼點擊按鈕,四個事件是如何處理的呢。首先如果點擊按鈕,最終走的就是extractEvents函數,一探究竟這個函數。

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;
        }
        /* 產生事件源對象 */
        const event = EventConstructor.getPooled(dispatchConfig,targetInst,nativeEvent,nativeEventTarget)
        const phasedRegistrationNames = event.dispatchConfig.phasedRegistrationNames;
        const dispatchListeners = [];
        const {bubbled, captured} = phasedRegistrationNames; /* onClick / onClickCapture */
        const dispatchInstances = [];
        /* 從事件源開始逐漸向上,查找dom元素類型HostComponent對應的fiber ,收集上面的React合成事件,onClick / onClickCapture  */
         while (instance !== null) {
              const {stateNode, tag} = instance;
              if (tag === HostComponent && stateNode !== null) { /* DOM 元素 */
                   const currentTarget = stateNode;
                   if (captured !== null) { /* 事件捕獲 */
                        /* 在事件捕獲階段,真正的事件處理函數 */
                        const captureListener = getListener(instance, captured);
                        if (captureListener != null) {
                        /* 對應發生在事件捕獲階段的處理函數,邏輯是將執行函數unshift添加到隊列的最前面 */
                            dispatchListeners.unshift(captureListener);
                            dispatchInstances.unshift(instance);
                            dispatchCurrentTargets.unshift(currentTarget);
                        }
                    }
                    if (bubbled !== null) { /* 事件冒泡 */
                        /* 事件冒泡階段,真正的事件處理函數,邏輯是將執行函數push到執行隊列的最後面 */
                        const bubbleListener = getListener(instance, bubbled);
                        if (bubbleListener != null) {
                            dispatchListeners.push(bubbleListener);
                            dispatchInstances.push(instance);
                            dispatchCurrentTargets.push(currentTarget);
                        }
                    }
              }
              instance = instance.return;
         }
          if (dispatchListeners.length > 0) {
              /* 將函數執行隊列,掛到事件對象event上 */
            event._dispatchListeners = dispatchListeners;
            event._dispatchInstances = dispatchInstances;
            event._dispatchCurrentTargets = dispatchCurrentTargets;
         }
        return event
    }
}

事件插件系統的核心extractEvents主要做的事是:

舉個例子比如如下

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  }  class >點擊</button>
    </div>
}

打印 // 4  2  1  3

看到這裏我們應該知道上述函數打印順序爲什麼了吧,首先遍歷 button 對應的 fiber,首先遇到了 onClickCapture , 將 handerClick1  放到了數組最前面,然後又把onClick對應handerClick的放到數組的最後面,形成的結構是[ handerClick1 , handerClick ] , 然後向上遍歷,遇到了div對應 fiber, 將onClickCapture對應的handerClick3放在了數組前面,將onClick對應的 handerClick2 放在了數組後面,形成的結構 [ handerClick3,handerClick1 , handerClick,handerClick2 ] , 所以執行的順序 // 4  2  1  3,就是這麼簡單,完美!

事件觸發

有的同學可能好奇 React 的事件源對象是什麼樣的,以上面代碼中SyntheticEvent爲例子我們一起來看看:

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單獨處理,阻止事件冒泡函數 */
    preventDefault(){ },  /* React單獨處理,阻止事件捕獲函數  */
    ...
}

handerClick 中打印 e :

既然事件執行隊列和事件源對象都形成了,接下來就是最後一步事件觸發了。上面大家有沒有注意到一個函數runEventsInBatch,所有事件綁定函數,就是在這裏觸發的。讓我們一起看看。

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()) { /* 判斷是否已經阻止事件冒泡 */
        break;
      }
      
      dispatchListeners[i](event)
    }
  }
  /* 執行完函數,置空兩字段 */
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

dispatchListeners[i](event)就是執行我們的事件處理函數比如handerClick, 從這裏我們知道,我們在事件處理函數中,返回 false ,並不會阻止瀏覽器默認行爲

handerClick(){ //並不能阻止瀏覽器默認行爲。
    return false
}

應該改成這樣:

handerClick(e){
    e.preventDefault()
}

另一方面 React 對於阻止冒泡,就是通過 isPropagationStopped,判斷是否已經阻止事件冒泡。如果我們在事件函數執行隊列中,某一會函數中,調用e.stopPropagation(),就會賦值給isPropagationStopped=()=>true,當再執行 e.isPropagationStopped()就會返回 true , 接下來事件處理函數,就不會執行了。

其他概念 - 事件池

 handerClick = (e) ={
    console.log(e.target) // button 
    setTimeout(()=>{
        console.log(e.target) // null
    },0)
}

對於一次點擊事件的處理函數,在正常的函數執行上下文中打印e.target就指向了dom元素,但是在setTimeout中打印卻是null,如果這不是 React 事件系統,兩次打印的應該是一樣的,但是爲什麼兩次打印不一樣呢?因爲在 React 採取了一個事件池的概念,每次我們用的事件源對象,在事件函數執行之後,可以通過releaseTopLevelCallbackBookKeeping等方法將事件源對象釋放到事件池中,這樣的好處每次我們不必再創建事件源對象,可以從事件池中取出一個事件源對象進行復用,在事件處理函數執行完畢後, 會釋放事件源到事件池中,清空屬性,這就是setTimeout中打印爲什麼是null的原因了。

事件觸發總結

我把事件觸發階段做的事總結一下:

五 關於 react v17 版本的事件系統

React v17 整體改動不是很大,但是事件系統的改動卻不小,首先上述的很多執行函數,在 v17 版本不復存在了。我來簡單描述一下 v17 事件系統的改版。

1 事件統一綁定 container 上,ReactDOM.render(app, container); 而不是 document 上,這樣好處是有利於微前端的,微前端一個前端系統中可能有多個應用,如果繼續採取全部綁定在document上,那麼可能多應用下會出現問題。

2 對齊原生瀏覽器事件

React 17中終於支持了原生捕獲事件的支持, 對齊了瀏覽器原生標準。同時 onScroll 事件不再進行事件冒泡。onFocusonBlur 使用原生 focusinfocusout 合成。

3 取消事件池React 17取消事件池複用,也就解決了上述在setTimeout打印,找不到e.target的問題。

六 總結

本文從事件合成事件綁定事件觸發三個方面詳細介紹了 React 事件系統原理,希望大家能通過這篇文章更加深入瞭解 v16 React 事件系統,如果有疑問和不足之處,也希望大家能在評論區指出。

最後, 送人玫瑰,手留餘香,覺得有收穫的朋友可以給筆者點贊,關注一波 ,陸續更新前端超硬核文章。

參考文檔

如果你覺得這篇內容對你挺有啓發,我想邀請你幫我三個小忙:

  1. 點個「在看」,讓更多的人也能看到這篇內容(喜歡不點在看,都是耍流氓 -_-)

  2. 歡迎加我微信「TH0000666」一起交流學習...

  3. 關注公衆號「前端 Sharing」,持續爲你推送精選好文。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/LvzC74rlYZr4tSkFkxGAoA