React事件機制 - 源碼概覽(上)

某次被問到 React事件機制的問題,關於這一塊我確實不怎麼清楚,由於平時大部分工做都是用 Vue,對於 React的熟悉程度只限於會用,具體實現邏輯還真沒專門學習過,可是總不能就說本身不清楚吧,好在我瞭解 Vue的事件機制,因而就把 Vue的事件機制說了一遍,最後再來一句「我以爲 React應該和 Vue的差很少」node

後來我想了下應該沒那麼簡單,因而網上搜了下相關文章,發現果真是被我想得太簡單了,Vue經過編譯模板,解析出事件指令,將事件和事件回調附加到 vnode tree上,在 patch過程當中的建立階段和更新階段都會對這個 vnode tree進行處理,拿到每一個 vnode上附加的事件信息,就能夠調用原生 DOM API對相應事件進行註冊或移除,流程仍是比較清晰的,而React則是單獨實現了一套事件機制react

本文以 React v16.5.2 爲基礎進行源碼分析數組

基本流程

react源碼的 react-dom/src/events/ReactBrowserEventEmitter.js文件的開頭,有這麼一大段註釋:瀏覽器

/** * Summary of `ReactBrowserEventEmitter` event handling: * * - Top-level delegation is used to ...... * ...... * * +------------+ . * | DOM | . * +------------+ . * | . * v . * +------------+ . * | ReactEvent | . * | Listener | . * +------------+ . +-----------+ * | . +--------+|SimpleEvent| * | . | |Plugin | * +-----|------+ . v +-----------+ * | | | . +--------------+ +------------+ * | +-----------.--->|EventPluginHub| | Event | * | | . | | +-----------+ | Propagators| * | ReactEvent | . | | |TapEvent | |------------| * | Emitter | . | |<---+|Plugin | |other plugin| * | | . | | +-----------+ | utilities | * | +-----------.--->| | +------------+ * | | | . +--------------+ * +-----|------+ . ^ +-----------+ * | . | |Enter/Leave| * + . +-------+|Plugin | * +-------------+ . +-----------+ * | application | . * |-------------| . * | | . * | | . * +-------------+ . * . * React Core . General Purpose Event Plugin System */
複製代碼

這段註釋第一段文本內容被我省略掉了,其主要是在大概描述 React的事件機制,也就是這個文件中的代碼要作的一些事情,大概意思就是說事件委託是很經常使用的一種瀏覽器事件優化策略,因而 React就接管了這件事情,而且還貼心地消除了瀏覽器間的差別,賦予開發者跨瀏覽器的開發體驗,主要是使用 EventPluginHub這個東西來負責調度事件的存儲,合成事件並以對象池的方式實現建立和銷燬,至於下面的結構圖形,則是對事件機制的一個圖形化描述緩存

根據這段註釋,大概能夠提煉出如下幾點內容:app

  • React事件使用了事件委託的機制,通常事件委託的做用都是爲了減小頁面的註冊事件數量,減小內存開銷,優化瀏覽器性能,React這麼作也是有這麼一個目的,除此以外,也是爲了可以更好的管理事件,實際上,React中全部的事件最後都是被委託到了 document這個頂級DOM
  • 既然全部的事件都被委託到了 document上,那麼確定有一套管理機制,全部的事件都是以一種先進先出的隊列方式進行觸發與回調
  • 既然都已經接管事件了,那麼不對事件作些額外的事情未免有些浪費,因而 React中就存在了本身的 合成事件(SyntheticEvent),合成事件由對應的 EventPlugin負責合成,不一樣類型的事件由不一樣的 plugin合成,例如 SimpleEvent PluginTapEvent Plugin
  • 爲了進一步提高事件的性能,使用了 EventPluginHub這個東西來負責合成事件對象的建立和銷燬

下文均如下述這段代碼爲示例進行分析:dom

export default class MyBox extends React.Component {
  clickHandler(e) {
    console.log('click callback', e)
  }
  render() {
    return (
      <div className="box" onClick={this.clickHandler}>文本內容</div>
    )
  }
}
複製代碼

事件註冊

只看相關主體流程,其餘諸如 vnode的建立等前提流程就無論了,從setInitialDOMProperties這個方法開始看起,這個方法主要用於遍歷 ReactNodeprops對象,給最後將要真正渲染的真實 DOM對象設置一系列的屬性,例如 styleclassautoFocus,也包括innerHTMLevent的處理等,示例中 .box元素的 props對象結構以下:函數

這個方法中有個 case,就是專門用於處理事件的:oop

// react-dom/src/client/ReactDOMComponent.js
else if (registrationNameModules.hasOwnProperty(propKey)) {
  if (nextProp != null) {
    if (true && typeof nextProp !== 'function') {
      warnForInvalidEventListener(propKey, nextProp);
    }
    // 處理事件類型的 props
    ensureListeningTo(rootContainerElement, propKey);
  }
}
複製代碼

其中的 registrationNameModules這個變量,裏面存在一大堆的屬性,都是與 React的事件相關:源碼分析

例子中的 onClick這個 props顯然符合,因此能夠執行 ensureListeningTo這個方法:

// react-dom/src/client/ReactDOMComponent.js
function ensureListeningTo(rootContainerElement, registrationName) {
  var isDocumentOrFragment = rootContainerElement.nodeType === DOCUMENT_NODE || rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
  var doc = isDocumentOrFragment ? rootContainerElement : rootContainerElement.ownerDocument;
  listenTo(registrationName, doc);
}
複製代碼

這個方法中,首先判斷了 rootContainerElement是否是一個 document或者 Fragment(文檔片斷節點),示例中傳過來的是 .box這個 div,顯然不是,因此 doc這個變量就被賦值爲 rootContainerElement.ownerDocument,這個東西其實就是 .box所在的 document元素,把這個document傳到下面的 listenTo裏了,事件委託也就是在這裏作的,全部的事件最終都會被委託到 document 或者 fragment上去,大部分狀況下都是 document,而後這個 registrationName就是事件名稱 onClick

接着開始執行 listenTo方法,這個方法其實就是註冊事件的入口了,方法裏面有這麼一句:

// react-dom/src/events/ReactBrowserEventEmitter.js
var dependencies = registrationNameDependencies[registrationName];
複製代碼

registrationName就是傳過來的 onClick,而變量 registrationNameDependencies是一個存儲了 React事件名與瀏覽器原生事件名對應的一個 Map,能夠經過這個 map拿到相應的瀏覽器原生事件名,registrationNameDependencies結構以下:

能夠看到,React是給事件名作了一些跨瀏覽器兼容事情的,好比傳入 onChange事件,會自動對應上 blur change click focus等多種瀏覽器原生事件

接下來,遍歷這個 dependencies數組,進入到如下 case

// react-dom/src/events/ReactBrowserEventEmitter.js
switch (dependency) {
  // 省略一些代碼
  default:
    // By default, listen on the top level to all non-media events.
    // Media events don't bubble so adding the listener wouldn't do anything.
    var isMediaEvent = mediaEventTypes.indexOf(dependency) !== -1;
    if (!isMediaEvent) {
      trapBubbledEvent(dependency, mountAt);
    }
    break;
}
複製代碼

除了 scroll focus blur cancel close方法走 trapCapturedEvent方法,invalid submit reset方法不處理以外,剩下的事件類型全走default,執行 trapBubbledEvent這個方法,trapCapturedEventtrapBubbledEvent兩者惟一的不一樣之處就在於,對於最終的合成事件,前者註冊捕獲階段的事件監聽器,然後者則註冊冒泡階段的事件監聽器

因爲大部分合成事件的代理註冊的都是冒泡階段的事件監聽器,也就是委託到 document上註冊的是冒泡階段的事件監聽器,因此就算你顯示聲明瞭一個捕獲階段的 React事件,例如 onClickCapture,此事件的響應也會晚於原生事件的捕獲事件以及冒泡事件 實際上,全部原生事件的響應(不管是冒泡事件仍是捕獲事件),都將早於 React合成事件(SyntheticEvent),對原生事件調用 e.stopPropagation()將阻止對應 SyntheticEvent的響應,由於對應的事件根本沒法到達document 這個事件委託層就被阻止掉了

兩者區別不大,trapBubbledEvent用的最多,本示例也將執行這個方法,因此就跟着這個方法看下去:

// react-dom/src/events/EventListener.js
// 對於本示例來講,topLevelType就是 click,element就是 document
function trapBubbledEvent(topLevelType, element) {
  if (!element) {
    return null;
  }
  var dispatch = isInteractiveTopLevelEventType(topLevelType) ? dispatchInteractiveEvent : dispatchEvent;

  addEventBubbleListener(element, getRawEventName(topLevelType),
  // Check if interactive and wrap in interactiveUpdates
  dispatch.bind(null, topLevelType));
}
複製代碼

addEventBubbleListener這個方法接收三個參數,在本示例中,第一個參數 element其實就是 document元素,getRawEventName(topLevelType)就是 click事件,第三個參數的 dispatch就是 dispatchInteractiveEventdispatchInteractiveEvent其實最後仍是會執行 dispatchEvent這個方法,只是在執行這個方法以前作了一些額外的事情,這裏不須要關心,能夠暫且認爲兩者是同樣的

看下 addEventBubbleListener這個方法:

// react-dom/src/events/EventListener.js
export function addEventBubbleListener( element: Document | Element, eventType: string, listener: Function, ): void {
  element.addEventListener(eventType, listener, false);
}
複製代碼

這個方法很簡單,就是用 addEventListenerdocument註冊了一個冒泡事件,listener這個事件的回調就是以前傳入 dispatch.bind(null, topLevelType)

流程圖以下:

事件分發

既然全部的事件都委託註冊到了 document上,那麼事件觸發的時候,確定須要一個事件分發的過程,來找到究竟是哪一個元素觸發的事件,並執行相應的回調函數,須要注意的是,因爲元素自己並無註冊任何事件,而是委託到了 document上,因此這個將被觸發的事件是 React自帶的合成事件,而非瀏覽器原生事件,但總之都是須要一個分發的過程的

在前面的 事件註冊 中已經提到過,註冊到 document上的事件,對應的回調函數都會觸發 dispatchEvent這個方法,進入這個方法:

// react-dom/src/events/ReactDOMEventListener.js
const nativeEventTarget = getEventTarget(nativeEvent);
let targetInst = getClosestInstanceFromNode(nativeEventTarget);
複製代碼

首先找到事件觸發的 DOMReact Component,找真實 DOM比較好找,直接取事件回調的 event參數的 target | srcElement | window便可,而後這個 nativeEventTarget對象上掛在了一個以 __reactInternalInstance開頭的屬性,這個屬性就是 internalInstanceKey,其值就是當前 React實例對應的 React Component

而後繼續往下看:

try {
  // Event queue being processed in the same cycle allows
  // `preventDefault`.
  batchedUpdates(handleTopLevel, bookKeeping);
} finally {
  releaseTopLevelCallbackBookKeeping(bookKeeping);
}
複製代碼

batchedUpdates,字面意思就是批處理更新,這裏實際上就是把當前觸發的事件放入了批處理隊列中,其中,handleTopLevel是事件分發的核心所在:

// react-dom/src/events/ReactDOMEventListener.js
let targetInst = bookKeeping.targetInst;

// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
let ancestor = targetInst;
do {
  if (!ancestor) {
    bookKeeping.ancestors.push(ancestor);
    break;
  }
  const root = findRootContainerNode(ancestor);
  if (!root) {
    break;
  }
  bookKeeping.ancestors.push(ancestor);
  ancestor = getClosestInstanceFromNode(root);
} while (ancestor);
複製代碼

首先在事件回調以前,根據當前組件,向上遍歷獲得其全部的父組件,存儲到 ancestors中,因爲全部的事件都委託到了 document上,因此在事件觸發後,不管是冒泡事件仍是捕獲事件,其在相關元素上的觸發確定是要有一個次序關係的,好比在子元素和父元素上都註冊了一個鼠標點擊冒泡事件,事件觸發後,確定是子元素的事件響應快於父元素,因此在事件隊列裏,子元素就要排在父元素前面,而在事件回調以前就要進行緩存,緣由在代碼的註釋裏也已經解釋得很清楚了,大概意思就是事件回調可能會改變 DOM結構,因此要先遍歷好組件層級關係,緩存起來

繼續往下:

// react-dom/src/events/ReactDOMEventListener.js
for (let i = 0; i < bookKeeping.ancestors.length; i++) {
  targetInst = bookKeeping.ancestors[i];
  runExtractedEventsInBatch(
    bookKeeping.topLevelType,
    targetInst,
    bookKeeping.nativeEvent,
    getEventTarget(bookKeeping.nativeEvent),
  );
}
複製代碼

使用了一個 for循環來遍歷這個 React Component及其全部的父組件,執行 runExtractedEventsInBatch方法,這裏的遍歷方法是從前日後遍歷,前面說了,咱們這裏分析的是 trapBubbledEvent,也就是冒泡事件,因此這裏對應到組件層級上就是由子元素到父元素,若是這裏是分析 trapCapturedEvent,即捕獲事件,那麼這個從前日後的順序就對應父元素到子元素了 提醒一點,不管是 trapBubbledEvent仍是 trapCapturedEvent,這裏都是針對 document元素而不是實際的元素,不要弄混了

至於循環中調用的 runExtractedEventsInBatch方法,其實就是事件執行的入口了

事件執行

runExtractedEventsInBatch這個方法中又調用了兩個方法:extractEventsrunEventsInBatchextractEvents用於構造合成事件,runEventsInBatch用於批處理 extractEvents構造出的合成事件

構造合成事件

找到合適的合成事件的 plugin

先看 extractEvents

// packages/events/EventPluginHub.js
let events = null;
for (let i = 0; i < plugins.length; i++) {
  // Not every plugin in the ordering may be loaded at runtime.
  const possiblePlugin: PluginModule<AnyNativeEvent> = plugins[i];
  if (possiblePlugin) {
    const extractedEvents = possiblePlugin.extractEvents(
      topLevelType,
      targetInst,
      nativeEvent,
      nativeEventTarget,
    );
    if (extractedEvents) {
      events = accumulateInto(events, extractedEvents);
    }
  }
}
複製代碼

首先遍歷 plugins,這個 plugins就是全部事件合成 plugins的集合數組,一共 5種(v15.x版本是 7種),這些 plugins都位於 react-dom/src/events這個文件夾下,以單獨文件的形式存在,文件名以 EventPlugin結尾的就是,它們是在 EventPluginHub初始化階段注入進去的:

// react-dom/src/client/ReactDOMClientInjection.js
EventPluginHub.injection.injectEventPluginsByName({
  SimpleEventPlugin: SimpleEventPlugin,
  EnterLeaveEventPlugin: EnterLeaveEventPlugin,
  ChangeEventPlugin: ChangeEventPlugin,
  SelectEventPlugin: SelectEventPlugin,
  BeforeInputEventPlugin: BeforeInputEventPlugin,
});
複製代碼

extractEvents方法裏用了一個 for循環,把全部的 plugin全都執行了一遍,我的理解沒這個必要,找到合適的 plugin執行完以後就能夠直接 break掉了 好比對於本示例的 click事件來講,合適的 pluginSimpleEventPlugin,其餘的 plugin就算是進入走了一遍也只是作了個無用功而已,由於執行完其餘 plugin後獲得的 extractedEvents都不知足 if (extractedEvents)這個條件,沒法給 events這個變量賦值或者覆蓋賦值,固然,也可能這段代碼還有其餘比較隱祕的做用吧

possiblePlugin.extractEvents 這一句就是調用相應 plugin的構造合成事件的方法,其餘的 plugin就不展開分析了,針對本示例的 SimpleEventPlugin,來看下它的 extractEvents

// react-dom/src/events/SimpleEventPlugin.js
const dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
  return null;
}
複製代碼

首先,看下 topLevelEventsToDispatchConfig這個對象中有沒有 topLevelType這個屬性,只要有,那麼說明當前事件可使用 SimpleEventPlugin構造合成事件,對於本示例來講,topLevelType就是 click,而topLevelEventsToDispatchConfig結構以下:

這些屬性就是一些常見的事件名,顯然 clicktopLevelEventsToDispatchConfig的一個屬性名,符合條件,能夠繼續往下執行,下面緊跟着的是一個 switch...case的判斷語句,對於本示例來講,將在下面這個 casebreak掉:

// react-dom/src/events/SimpleEventPlugin.js
case TOP_CLICK:
  // 省略了一些代碼
  EventConstructor = SyntheticMouseEvent;
  break;
複製代碼

SyntheticMouseEvent能夠看作是 SimpleEventPlugin的一個具體的子 plugin,至關因而對 SimpleEventPlugin這個大概念的 plugin又細分了一層,除了 SyntheticMouseEvent以外還有 SyntheticWheelEventSyntheticClipboardEventSyntheticTouchEvent

從合成事件對象池中取對象

設置好具體的 EventConstructor後,繼續往下執行:

// react-dom/src/events/SimpleEventPlugin.js
const event = EventConstructor.getPooled(
  dispatchConfig,
  targetInst,
  nativeEvent,
  nativeEventTarget,
);
accumulateTwoPhaseDispatches(event);
return event;
複製代碼

getPooled就是從 event對象池中取出合成事件,這種操做是 React的一大亮點,將全部的事件緩存在對象池中,能夠大大下降對象建立和銷燬的時間,提高性能

getPooledEventConstructor上的一個方法,這個方法是在 EventConstructor初始化的時候掛上去的,但歸根到底,這個方法是位於 SyntheticEvent這個對象上,流程示意圖以下:

這個 getPooled其實就是 getPooledEvent,在 SyntheticEvent初始化的過程當中就被設置好初始值了:

// packages/events/SyntheticEvent.js
addEventPoolingTo(SyntheticEvent);
// 省略部分代碼
function addEventPoolingTo(EventConstructor) {
  EventConstructor.eventPool = [];
  EventConstructor.getPooled = getPooledEvent;
  EventConstructor.release = releasePooledEvent;
}
複製代碼

那麼看下 getPooledEvent

// packages/events/SyntheticEvent.js
function getPooledEvent(dispatchConfig, targetInst, nativeEvent, nativeInst) {
  const EventConstructor = this;
  if (EventConstructor.eventPool.length) {
    const instance = EventConstructor.eventPool.pop();
    EventConstructor.call(
      instance,
      dispatchConfig,
      targetInst,
      nativeEvent,
      nativeInst,
    );
    return instance;
  }
  return new EventConstructor(
    dispatchConfig,
    targetInst,
    nativeEvent,
    nativeInst,
  );
}
複製代碼

首次觸發事件的時候(在本示例中就是 click事件),EventConstructor.eventPool.length0,由於這個時候是第一次事件觸發,對象池中沒有對應的合成事件引用,因此須要初始化,後續再觸發事件的時候,就無需 new了,而是走上面那個邏輯,直接從對象池中取,經過 EventConstructor.eventPool.pop();獲取合成對象實例

這裏先看下初始化的流程,會執行 new EventConstructor這一句,前面說了,這個東西能夠看作是 SyntheticEvent的子類,或者是由 SyntheticEvent擴展而來的東西,怎麼擴展的呢,其實是使用了一個 extend方法:

const SyntheticMouseEvent = SyntheticUIEvent.extend({
  screenX: null,
  screenY: null,
  clientX: null,
  clientY: null,
  pageX: null,
  pageY: null,
  // 省略部分代碼
})
複製代碼

首先,SyntheticMouseEvent這個合成事件,有本身的一些屬性,這些屬性其實和瀏覽器原生的事件回調參數對象 event的屬性沒多大差異,都有對於當前事件的一些描述,甚至連屬性名都同樣,只不過相比於瀏覽器原生的事件回調參數對象 event來講,SyntheticMouseEvent 或者說 合成事件SyntheticEvent的屬性是由 React主動生成,通過 React的內部處理,使得其上附加的描述屬性徹底符合 W3C的標準,所以在事件層面上具備跨瀏覽器兼容性,與原生的瀏覽器事件同樣擁有一樣的接口,也具有stopPropagation()preventDefault()等方法

對於本示例中的點擊事件回調方法來講:

clickHandler(e) {
  console.log('click callback', e)
}
複製代碼

其中的 e其實就是 合成事件而非瀏覽器原生事件的 event,因此開發者無需考慮瀏覽器兼容性,只須要按照 w3c規範取值便可,若是須要訪問原生的事件對象,能夠經過 e.nativeEvent 得到

SyntheticUIEvent這個東西主要就是往 SyntheticMouseEvent上加一些額外的屬性,這裏不用關心,而後這個 SyntheticMouseEvent.extend又是由 SyntheticEvent擴展 (extend)來的,因此最終會 new SyntheticEvent

先看下 extend方法:

// packages/events/SyntheticEvent.js
SyntheticEvent.extend = function(Interface) {
  const Super = this;

  // 原型式繼承
  const E = function() {};
  E.prototype = Super.prototype;
  const prototype = new E();
  // 構造函數繼承
  function Class() {
    return Super.apply(this, arguments);
  }
  Object.assign(prototype, Class.prototype);
  Class.prototype = prototype;
  Class.prototype.constructor = Class;

  Class.Interface = Object.assign({}, Super.Interface, Interface);
  Class.extend = Super.extend;
  addEventPoolingTo(Class);

  return Class;
};
複製代碼

先來了個經典的寄生組合式繼承,這種寄生方法最爲成熟,大多數庫都是使用這種繼承方法,React這裏也用了它,讓EventConstructor繼承於 SyntheticEvent,得到 SyntheticEvent上的一些屬性和方法,如前面所說的 eventPoolgetPooled

既然存在繼承關係,那麼 new EventConstructor這個子類,天然就會調用父類 SyntheticEventnew方法,也就是開始調用合成組件的構造器了,開始真正構造合成事件,主要就是將原生瀏覽器事件上的參數掛載到合成事件上,包括 clientXscreenYtimeStamp等事件屬性, preventDefaultstopPropagation等事件方法,例如前面所說的經過 e.nativeEvent得到的原生事件就是在這個時候掛載上去的:

// packages/events/SyntheticEvent.js
this.nativeEvent = nativeEvent;
複製代碼

掛載的屬性都是通過 React處理過的,具有跨瀏覽器能力,一樣,掛載的方法也和原生瀏覽器的事件方法有所不一樣,由於此時的事件附加在 document上的,因此調用一些事件方法,例如 e.stopPropagation()實際上是針對 document元素調用的,跟本來指望的元素不是同一個,那麼爲了讓合成事件的表現達到原生事件的效果,就須要對這些方法進行額外的處理

處理的方法也比較簡單,就是加了一個標誌位,例如,對於 stopPropagation來講, React對其進行了包裝:

// packages/events/SyntheticEvent.js
stopPropagation: function() {
  const event = this.nativeEvent;
  if (!event) {
    return;
  }

  if (event.stopPropagation) {
    event.stopPropagation();
  } else if (typeof event.cancelBubble !== 'unknown') {
    // The ChangeEventPlugin registers a "propertychange" event for
    // IE. This event does not support bubbling or cancelling, and
    // any references to cancelBubble throw "Member not found". A
    // typeof check of "unknown" circumvents this issue (and is also
    // IE specific).
    event.cancelBubble = true;
  }

  this.isPropagationStopped = functionThatReturnsTrue;
}
複製代碼

首先就是拿到瀏覽器原生事件,而後調用對應的 stopPropagation方法,這裏須要注意一下,這裏的 event是由 document這個元素上的事件觸發而生成的事件回調的參數對象,而非實際元素的事件回調的參數對象,說得明白點,就是給document上觸發的事件,例如點擊事件,調用了一下 e.stopPropagation,阻止事件繼續往 document 或者 Fragment 的父級傳播

// packages/events/SyntheticEvent.js
// 這個函數其實就是返回了一個 true,與此對應的,還有個函數名爲 functionThatReturnsFalse的函數,用來返回 false
function functionThatReturnsTrue() {
  return true;
}
複製代碼

關鍵在於 this.isPropagationStopped = functionThatReturnsTrue;這一句,至關因而設置了一個標誌位,對於冒泡事件來講,當事件觸發,由子元素往父元素逐級向上遍歷,會按順序執行每層元素對應的事件回調,但若是發現當前元素對應的合成事件上的 isPropagationStoppedtrue值,則遍歷的循環將中斷,也就是再也不繼續往上遍歷,當前元素的全部父元素的合成事件就不會被觸發,最終的效果,就和瀏覽器原生事件調用 e.stopPropagation()的效果是同樣的

捕獲事件的原理與此相同,只不過是由父級往子級遍歷的罷了

這些事件方法(包括 stopPropagationpreventDefault等)通常都是在事件回調函數內調用,而事件的回調函數則是在後面的批處理操做中執行的

var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
accumulateTwoPhaseDispatches(event);
複製代碼

拿到全部與當前觸發事件相關的元素實例和事件回調函數

上述一大堆都是從上述代碼的第一句 getPooled爲入口進去的,主要是爲了獲得合成事件,拿到基本的合成事件之後,開始對這個合成事件進行進一步的加工,也就是 accumulateTwoPhaseDispatches這個方法要作的事情,這個方法涉及到的流程比較多,畫個圖清晰點:

代碼和調用的方法都比較瑣碎,但目標很清晰,就是保存當前元素及其父元素上掛在的全部事件回調函數,包括捕獲事件(captured)和冒泡事件(bubbled),保存到事件event_dispatchListeners屬性上,而且將當前元素及其父元素的react實例(在 v16.x版本中,這裏的實例是一個 FiberNode)保存到event_dispatchInstances屬性上

拿到了全部與事件相關的元素實例以及事件的回調函數以後,就能夠對合成事件進行批量處理了

因爲 React的事件機制比較複雜,要說的地方有點多,因此分爲了兩篇文章,剩餘分析部分請參見文章 React事件機制 - 源碼概覽(下)

相關文章
相關標籤/搜索