react事件源碼步步調(diào)

本文通過一個簡短的實例&控制臺調(diào)試,了解react事件處理的全過程。下面是測試用代碼,使用控制臺可以清晰看到函數(shù)執(zhí)行過程中參數(shù)變化以及方法所屬模塊&調(diào)用棧,所以本文圖片較多。


class RemoveBtn extends Component {
    clickHandler = () => {
        this.props.handleClick();
    }
    render(){
        return(
            <button onClick={this.clickHandler}>togglage測試組件</button>
        )
    }
}

class Root extends Component {
    
    clickHandler = () => {
        alert('hanlder is1 perform')
    }
    render(){
        return (
            <div className="first">
                <RemoveBtn handleClick = {this.clickHandler}/>
            </div>
        )
    } 
}

ReactDOM.render(<Root />, document.getElementById('root'));

1 事件綁定

1.1 綁定的結(jié)果

事件綁定結(jié)果

說明: 這里的backend.js是react調(diào)試工具的腳本不用考慮。

圖中可見只有在document上綁定了名為dispatchEvent的來自于 ReactEventListener.js模塊的事件處理函數(shù)。

1.2 事件綁定的過程

ReactDOM.render(<Root />, document.getElementById('root'));

一切開始于ReactDOM.render調(diào)用的ReactMount.jsrender方法。忽略掉實例化組建的過程,詳細調(diào)用可以查看截圖右側(cè)的調(diào)用棧。

ReactDom.render 將react組件渲染到指定的容器上

_renderSubtreeIntoContainer -> mountComponentIntoNode -> mountComponent[reactReconciler.js] -> _updateDOMProperties

判斷綁定事件還是刪除事件-w1564

_updateDOMProperties函數(shù)在mountComponent,unmountComponentupdateComponent階段都有調(diào)用,它是檢查屬性變化,調(diào)優(yōu)性能的重要方法。下圖節(jié)選處理事件綁定部分代碼,方法中有指向上次屬性值得lastProp, nextProp是當(dāng)前屬性值,這里nextProp是我們綁定給組件的onclick事件處理函數(shù)。nextProp 不為空調(diào)用enqueuePutListener綁定事件為空則注銷事件綁定。

queuePutListener

enqueuePutListener 這個方法只在瀏覽器環(huán)境下執(zhí)行,傳給listenTo參數(shù)分別是事件名稱'onclick'和代理事件的綁定dom。如果是fragement 就是根節(jié)點(在reactDom.render指定的),不是的話就是documentlistenTo 用于綁定事件到 document ,下面交由事務(wù)處理的是回調(diào)函數(shù)的存儲,便于調(diào)用。ReactBrowserEventEmitter 文件中的 listenTo 看做事件處理的源頭。

listenTo
  listenTo: function (registrationName, contentDocumentHandle) {
    var mountAt = contentDocumentHandle;
    var isListening = getListeningForDocument(mountAt);
    // 獲取 registrationName(注冊事件名稱)的topLevelEvent(頂級事件類型)
    var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

    for (var i = 0; i < dependencies.length; i++) {
      var dependency = dependencies[i];
      if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
        if (dependency === 'topWheel') {
           ...         
        } else if (dependency === 'topScroll') {
               ...
        } else if (dependency === 'topFocus' || dependency === 'topBlur') {
                ...
        } else if (topEventMapping.hasOwnProperty(dependency)) {
        // 獲取 topLevelEvent 對應(yīng)的瀏覽器原生事件
          ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
        }
        isListening[dependency] = true;
      }
    }
  },

對于同一個事件,例如click有兩個事件 onClick(在冒泡階段觸發(fā)) onClickCapture(在捕獲階段觸發(fā))兩個事件名,這個冒泡和捕獲都是react事件模擬出來的。綁定到 document上面的事件基本上都是在冒泡階段(對 whell, focus, scroll 有額外處理),如下圖 click 事件綁定執(zhí)行的如下。

listenTo

topEventMappingtopLevlelEvent 瀏覽器事件對照關(guān)系,mountAt 是綁定對象是函數(shù)接收第二個參數(shù),也就是上文的doc(document)。

ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 對所傳的target做了非空判斷后調(diào)用 EventListener.listen 傳參數(shù)分別是:事件對象, 瀏覽器原生事件名稱, 指定了頂級事件類型的事件處理函數(shù)(bind函數(shù))ReactEventListener.dispatchEvent.bind(null, topLevelType)

EventListener.listen

EventListener.listen 將事件綁定到target上。
回到上文利用事務(wù)存儲事件部分,這里調(diào)用的putListener方法

調(diào)用 EventPluginHub.putListener 第一個參數(shù)是組件事例,第二個是‘onClick’,第三個是我們寫的事件處理函數(shù)

listenerBank存儲的listener

putListener 將事件處理函數(shù)存儲到listenerBank[registrationName][key]上其中registrationName是事件名稱,.${_rootNodeID}``作為key值,處理函數(shù)作為value存儲。下面調(diào)用的方法有對與safraiclick`事件的兼容處理。
至此事件綁定告一段落了。

2 事件處理

event pooling事件池
合成事件是 pooled(循環(huán)使用的),這意味著合成事件對象會被重復(fù)使用,所有的屬性在被調(diào)用以后會被值為null,該機制用于性能優(yōu)化,因此你不可以異步訪問事件。除非調(diào)用 event.persist(),該方法不會不會把事件放入事件池中,保持event對象不被重置允許代碼的引用到。

事件觸發(fā)后執(zhí)行dispatchEvent方法,該方法第一個參數(shù)是綁定時bind的 topLevelEvent這里是 topClick,此處調(diào)用TopLevelCallbackBookKeeping.getPooled函數(shù)先去事件池中取可以復(fù)用的,沒有的話初始化新的。

這個bookKeeping初始化很簡單,就是把頂級事件類型,原生事件對象,空的父組件列表放在一個對象上。

獲取bookKeeping-w1062

reactUpdate.batchedUpdates是用事務(wù)封裝了handleTopLevelImpl(bookKeeping)。

getEventTarget 返回的是對應(yīng)的Dom節(jié)點
ReactDOMComponentTree.getClosestInstanceFromNode 返回對應(yīng)的 reactDomComponent

執(zhí)行事件回調(diào)前,先由當(dāng)前組件向上遍歷它的所有父組件。保存到bookKeeping.ancestors這個數(shù)組中。因為事件回調(diào)中可能會改變DOM結(jié)構(gòu),所以要先遍歷好組件層級,防止與已緩存ReactMount's node相矛盾。之后就是依次掉調(diào)用 ReactEventListener._handleTopLevel

最后一個參數(shù)通過getEventTarget函數(shù)兼容svg以及safraitextNode 這里最終返回的是觸發(fā)事件的DOM節(jié)點。

handleTopLevel函數(shù)經(jīng)由EventPluginHub處理 top level Event,在EventPluginHub處理過程中不同的plugin可以創(chuàng)建派發(fā)相應(yīng)的事件。第一行是構(gòu)造出合成事件,第二行就是交由事務(wù)處理事件。

2.1 構(gòu)建react事件

extractEvent 讓已注冊的plugin處理相應(yīng)的的topLevelType。下圖看到在運行過程中已注冊的plugin只有五個分別是

 ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
  });

extractEvent會依次調(diào)用每個pluginextractEvents方法,第一個處理的是SimpleEventPlugin,該plugin處理了絕大部分的事件,本例 onClick 就是其中之一。

SimpleEventPlugin.extractEvent

經(jīng)由一個switch(topLevelType)確定該react事件的構(gòu)造函數(shù)為SyntheticMouseEvent

SimpleEventPlugin.extractEvent 根據(jù) topLevelEvent 處理事件

上文看到 topClick 使用 syntheticMouseEvent 作為事件構(gòu)造函數(shù)。

這里調(diào)用的EventConstructor.getPooled就是開篇提到的事件池,先看有沒有可以復(fù)用的事件對象沒有的話在重新實例一個。

-w944

這里SyntheticMouseEvent調(diào)用 SyntheticUIEvent, SyntheticUIEvent調(diào)用 SyntheticEventSyntheticEvent構(gòu)造函數(shù)這部分代碼相對較長,函數(shù)注釋中說道,該方法應(yīng)該盡量減少調(diào)用的頻率,使用pooling(回收再利用|池)機制。在構(gòu)建時候會通過判斷isPersistent屬性來判斷調(diào)用后是否放入池中。使用者可以通過調(diào)用 persist方法來改變這個值。
而后執(zhí)行的是 EventPropagators.accumulateTwoPhaseDispatches(event)
這個方法經(jīng)歷層層跳轉(zhuǎn),詳情可見調(diào)用棧,最后到traverseTwoPhase這個函數(shù)。inst 為 觸發(fā)事件的reactDomComponent,fnaccumulateDirectionDispatches, arg 為合成事件。

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._hostParent;
  }
  var i;
  for (i = path.length; i-- > 0;) {
    fn(path[i], 'captured', arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], 'bubbled', arg);
  }
}

path 為收集的以target起始到根節(jié)點為止的組件,本例中兩個。用于后續(xù)模擬事件的捕獲和冒泡。

traverseTwoPhase

之后按照從外到內(nèi)捕獲從里到外冒泡的順序調(diào)用 accumulateDirectionDispatches(path[i], 'captured', arg)該方法將合成事件與處理函數(shù)聯(lián)系起來。

這里 listenerAtPhase -> getListener[EventPluginHub.js] 獲取事件處理函數(shù)。
在事件綁定中最后把所有的事件處理放在一個對象上listenerBank

-w594

通過注冊類型獲取到對應(yīng)類型的所有處理函數(shù),使用.${reactDomComponent._rootNodeID} 找到對應(yīng)虛擬Dom上的事件處理函數(shù)。

/**
  * @param {object} inst reactDOMComponent 實例 (虛擬DOM)
  * @param {string} registrationName 注冊事件名
  * * /
 getListener: function (inst, registrationName) {
    // TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
    // live here; needs to be moved to a better place soon
    // 獲取同類型的所有處理函數(shù)
    var bankForRegistrationName = listenerBank[registrationName];
    if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
      return null;
    }
    // 獲取 .${reactDomComponent._rootNodeID}`
    var key = getDictionaryKey(inst);
    // 返回指定虛擬DOM上的事件處理函數(shù)
    return bankForRegistrationName && bankForRegistrationName[key];
  }


獲取事件處理函數(shù)后,將它和響應(yīng)的reactDOMComponent分別添加到隊列中。accumulateInto用于將內(nèi)容添加到現(xiàn)有隊列中,傳入原來隊列和要添加到隊列中的內(nèi)容。

accumulateInto

至此事件已經(jīng)封裝準(zhǔn)備好了。

2.2 事件分發(fā)

事件分發(fā)

承接上文封裝好的event對象。使用runEventQueueInBatch開始事件分發(fā)。

這里第一行用于將事件放入隊列processEventQueue中,其內(nèi)部調(diào)用的還是accumulateInto方法。
第二行,processEventQueue派發(fā)所有在事件隊列processEventQueue中的合成事件。

首先將隊列中的內(nèi)容取出,清空隊列,以防處理中隊列變化。

simulated:為true表示React測試代碼,我們一般都是false
此注解出自參考文章一

這里forEachAccumulate就是對第一個參數(shù)執(zhí)行foreach調(diào)用第二個參數(shù)。

executeDispatchesAndReleaseTopLevel -> executeDispatchesAndRelease 該函數(shù) -> EventPluginUtils.executeDispatchesInOrder,并將沒有調(diào)用persist的事件對象回收到事件池。

EventPluginUtils.executeDispatchesInOrder

處理函數(shù)是多個,則依次執(zhí)行。本例中只有一個處理函數(shù) -> executeDispatch。執(zhí)行后設(shè)置 event._dispatchListenerevent._dispatchInstances 為 null。

executeDispatch

通過EventPluginUtils.getNodeFromInstance獲取響應(yīng)的對應(yīng)的真實DOM節(jié)點作為事件的currentTarget。
本例執(zhí)行85行 這里的type 為click,func為事件處理函數(shù), event為合成事件對象。
在生產(chǎn)環(huán)境中,會直接調(diào)用事件處理函數(shù),開發(fā)環(huán)境中會模擬瀏覽器事件。

模擬過程如下。

這里在創(chuàng)建的fakeElement上綁定事件,之后模擬事件觸發(fā)(執(zhí)行本例中的事件處理函數(shù)),再注銷事件綁定。

到此為止這個事件已經(jīng)處理完,接下來就是把這個事件屬性置為null,然后把它放入事件池中了。判斷是否強制了調(diào)用了persistent,沒有的話就釋放事件對象。

其實這里可以看到事件池有一個上線就是10,當(dāng)可用的對象大于10也不會再往里面添加了。
最后看一下事件的 destructor 方法

這里獲取所有的屬性設(shè)置為null,并且再訪問該事件對象時會預(yù)警提醒。

至此事件處理完成。

3 事件機制總結(jié)

這里是源碼注釋的翻譯

  • 頂級代理是用于捕獲多數(shù)原生瀏覽器事件,這些只會在主線程發(fā)生,并由reactEventLister 負責(zé)處理,reactEventLister 是被注入的因此可以支持插件事件資源,這是唯一在主線程執(zhí)行的。
  • 封裝了頂層事件(TopLevelEvent)來應(yīng)對瀏覽器異常。這個在工作線程完成。
  • 傳遞原生事件以及封裝的頂層事件名稱到 EventPluginHub,他會遍歷插件是否要執(zhí)行某些合成事件。
  • EventPluginHub 獲取響應(yīng)的事件監(jiān)聽器,以及Dom綁定到生成的事件對象上。
  • EventPluginHub 將會派發(fā)事件

3.1 各種事件名

主要三個事件:regiestrationName(注冊事件名),topLevelType(頂層事件),(原生事件)

事件綁定階段,從組件屬性中獲取’注冊事件名‘,會區(qū)分捕獲和默認冒泡事件名,這里的注冊名為react對外暴露的事件,包含自定義事件。
頂層事件是react封裝EventPlugin處理的單位,react對外暴露的事件是由一個多個事件模擬而成的。
原生事件是最終綁定到目標(biāo)元素上的事件,和頂層事件對應(yīng)關(guān)系為一對一的關(guān)系。在綁定給document的是使用bind函數(shù),固定第一個參數(shù)——topLevelEvent的函數(shù)。因此當(dāng)事件觸發(fā)后使用的。

// 本例中
// regiestrationName(注冊名)
onClick
onClickCapture

// topLevelType (頂層事件類型)
topClick

// native Event (原生事件) | dependence
click

// regiestrationName => topLevelType
EventPluginRegisterName.registionNameDependencies

// topLevelType => native event
topEventMapping[位于reactBrowserEventEmitter.js]

3.2 事件全局代理(target)

根據(jù)不同的topLevelType對應(yīng)的瀏覽器事件,綁定到target上(如果是fragement 就是根節(jié)點(在reactDom.render指定的),不是的話就是document)ReactEventListener.dispatchEvent.bind(null, topLevelType)。

3.3 事件存儲

當(dāng)組件渲染和更新的時候會調(diào)用_updateDomPorperties方法檢查屬性變化,這里執(zhí)行reactBrowerEventEmitter模塊下的listenTo對不同事件進行了兼容處理后最終調(diào)用 EventPluginHub.js模塊下的 putListener方法,將事件處理函數(shù),以 .${reactDomComponent._rootNodeID}為key值放在listenerBank[registrationName]對象上。

3.4 阻止事件冒泡

通過事件綁定的分析會發(fā)現(xiàn),無論注冊的是onClick 還是 onClickCapture 最后都是調(diào)用 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent 在冒泡階段觸發(fā)的事件, 也會發(fā)現(xiàn)在沒有執(zhí)行事件處理函數(shù)的時候,事件就已經(jīng)eventQuene中,那是不是就意味著調(diào)用e.stopPropagation()就不能阻止事件冒泡了呢。

事件處理函數(shù)中獲取的事件是合成事件對象,合成事件對象也是有stopPropagation方法的。

合成事件對象的stopPropagation方法

注意這里的最后一行,這里執(zhí)行的函數(shù)為事件isPropagationStopped方法賦值了一個只會返回true的函數(shù)。而在一次調(diào)用事件處理函數(shù)的過程中,每一次都會調(diào)用事件對象的該方法。

因此使用e.stopPropagation()不能組織原生事件冒泡,但是模擬到阻止事件冒泡的效果的。

react 文檔說明
更多可參考[4]

3.5 事件相關(guān)文件

synthetcEvent 封裝合成事件基類

原型方法:

  • preventDefault()
  • stopPropergation()
  • persist() 調(diào)用后isPersist = true, 此事件對象將不會被銷毀復(fù)用(進入事件池)
  • isPersist
  • desturctor() 事件觸發(fā)后(isPersist!==true), 清空事件對象屬性。

**靜態(tài)方法: **

  • arugumentClass
// @prarm interface 需要定義的事件對象屬性
// @param Class 子類
SyntheticEvent.augmentClass = function(Class, Interface) {
  var Super = this;
  var E = function() {};
  E.prototype = Super.prototype;
  var prototype = new E();

  Object.assign(prototype, Class.prototype);
  // 子類繼承基類原型上的方法
  Class.prototype = prototype;
  Class.prototype.constructor = Class;
    // 合并interface
  Class.Interface = Object.assign({}, Super.Interface, Interface);
  Class.augmentClass = Super.augmentClass;
  // 為子類添加事件池相關(guān)屬性和方法
  addEventPoolingTo(Class);
};
  • eventPool[]
  • getPooled()
    參數(shù)同構(gòu)造函數(shù)傳參,判斷事件池中是否有可用事件,有的復(fù)用,沒有新建。
  • release(event)
    判斷事件對象是否 isPersist過 沒有的話調(diào)用對象的 destructor, 之后將其添加入事件池。

參考
React源碼分析7 — React合成事件系統(tǒng)
看源碼react事件機制
React源碼解讀系列 – 事件機制
react 合成事件和原生事件的阻止冒泡

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Android 自定義View的各種姿勢1 Activity的顯示之ViewRootImpl詳解 Activity...
    passiontim閱讀 178,781評論 25 709
  • 版權(quán)聲明:本文為博主原創(chuàng)文章,未經(jīng)博主允許不得轉(zhuǎn)載。 PS:轉(zhuǎn)載請注明出處作者:TigerChain地址:http...
    TigerChain閱讀 8,469評論 1 9
  • 原教程內(nèi)容詳見精益 React 學(xué)習(xí)指南,這只是我在學(xué)習(xí)過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,931評論 1 18
  • 成果: Django的簡介 Django的基本教程這個是菜鳥教程中的,包含了安裝和一些基本的使用,講的還可以 介紹...
    泠泠七弦客閱讀 442評論 0 0
  • 在師父的教導(dǎo)下,我對代碼的書寫規(guī)范也是越來越有強迫癥了。我的代碼若能有幸被你看見,則不難發(fā)現(xiàn)有一些規(guī)律。 *.ht...
    依暄閱讀 2,024評論 8 8

友情鏈接更多精彩內(nèi)容