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

清夜發表於2018-10-26

某次被問到 React事件機制的問題,關於這一塊我確實不怎麼清楚,因為平時大部分工作都是用 Vue,對於 React的熟悉程度只限於會用,具體實現邏輯還真沒專門學習過,但是總不能就說自己不清楚吧,好在我瞭解 Vue的事件機制,於是就把 Vue的事件機制說了一遍,最後再來一句“我覺得 React應該和 Vue的差不多”

後來我想了下應該沒那麼簡單,於是網上搜了下相關文章,發現果然是被我想得太簡單了,Vue通過編譯模板,解析出事件指令,將事件和事件回撥附加到 vnode tree上,在 patch過程中的建立階段和更新階段都會對這個 vnode tree進行處理,拿到每個 vnode上附加的事件資訊,就可以呼叫原生 DOM API對相應事件進行註冊或移除,流程還是比較清晰的,而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這個東西來負責排程事件的儲存,合成事件並以物件池的方式實現建立和銷燬,至於下面的結構圖形,則是對事件機制的一個圖形化描述

根據這段註釋,大概可以提煉出以下幾點內容:

  • React事件使用了事件委託的機制,一般事件委託的作用都是為了減少頁面的註冊事件數量,減少記憶體開銷,優化瀏覽器效能,React這麼做也是有這麼一個目的,除此之外,也是為了能夠更好的管理事件,實際上,React中所有的事件最後都是被委託到了 document這個頂級DOM
  • 既然所有的事件都被委託到了 document上,那麼肯定有一套管理機制,所有的事件都是以一種先進先出的佇列方式進行觸發與回撥
  • 既然都已經接管事件了,那麼不對事件做些額外的事情未免有些浪費,於是 React中就存在了自己的 合成事件(SyntheticEvent),合成事件由對應的 EventPlugin負責合成,不同型別的事件由不同的 plugin合成,例如 SimpleEvent PluginTapEvent Plugin
  • 為了進一步提升事件的效能,使用了 EventPluginHub這個東西來負責合成事件物件的建立和銷燬

下文均以下述這段程式碼為示例進行分析:

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物件結構如下:

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

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

// 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的事件相關:

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事件機制 - 原始碼概覽(上)

可以看到,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)

流程圖如下:

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

事件分發

既然所有的事件都委託註冊到了 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結構如下:

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

這些屬性就是一些常見的事件名,顯然 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這個物件上,流程示意圖如下:

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

這個 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這個方法要做的事情,這個方法涉及到的流程比較多,畫個圖清晰點:

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

程式碼和呼叫的方法都比較瑣碎,但目標很清晰,就是儲存當前元素及其父元素上掛在的所有事件回撥函式,包括捕獲事件(captured)和冒泡事件(bubbled),儲存到事件event_dispatchListeners屬性上,並且將當前元素及其父元素的react例項(在 v16.x版本中,這裡的例項是一個 FiberNode)儲存到event_dispatchInstances屬性上

拿到了所有與事件相關的元素例項以及事件的回撥函式之後,就可以對合成事件進行批量處理了

由於 React的事件機制比較複雜,要說的地方有點多,所以分為了兩篇文章,剩餘分析部分請參見文章 React事件機制 - 原始碼概覽(下)

相關文章