首頁 > 軟體

深入分析React原始碼中的合成事件

2022-11-11 14:00:34

熱身準備

明確幾個概念

React@17.0.3版本中:

  • 所有事件都是委託在id = root的DOM元素中(網上很多說是在document中,17版本不是了);
  • 在應用中所有節點的事件監聽其實都是在id = root的DOM元素中觸發;
  • React自身實現了一套事件冒泡捕獲機制;
  • React實現了合成事件SyntheticEvent
  • React17版本不再使用事件池了(網上很多說使用了物件池來管理合成事件物件的建立銷燬,那是16版本及之前);
  • 事件一旦在id = root的DOM元素中委託,其實是一直在觸發的,只是沒有繫結對應的回撥函數;

盜用一張官方圖,按官方解釋,之所以會將事件委託從document中移到id = root的DOM元素,是為了可以更加安全地進行新舊版本 React 樹的巢狀

感興趣的可以存取:React中文網站 。

事件系統角色劃分

  • 事件註冊:registerEvents
  • 事件監聽:listenToAllSupportedEvents
  • 事件合成:SyntheticBaseEvent
  • 事件派發:dispatchEvent

事件註冊

事件註冊是自執行的,也就是React自身進行呼叫的:

// 註冊React事件
registerSimpleEvents();  
registerEvents$2();
registerEvents$1();
registerEvents$3();
registerEvents();

React事件就是在元件中呼叫的onClick這種寫法的事件。上面分為5個函數寫,主要是區分不同的事件註冊邏輯,但是最後都會新增到allNativeEvents的Set資料結構中。

registerSimpleEvents

這裡會註冊大部分事件,它們在React被定義為頂級事件。

它們分為三類:

  • 離散事件:discreteEvent,常見的如:click, keyup, change
  • 使用者阻塞事件:userBlocking,常見的如:dragEnter, mouseMove, scroll
  • 連續事件:continuous,常見的如:error, progress, load, ; 它們的優先順序排序:

0:離散事件, 1:使用者阻塞事件, 2:連續事件

它們會註冊冒泡和捕獲階段兩個事件。

registerEvents$2

註冊類似onMouseEnteronMouseLeave單階段事件,只註冊冒泡階段事件。

registerEvents$1

註冊onChange相關事件,註冊冒泡和捕獲階段兩個事件。

registerEvents$3

註冊onSelect相關事件,註冊冒泡和捕獲階段兩個事件。

registerEvents

註冊onBeforeInputonCompositionUpdate等相關事件,註冊冒泡和捕獲階段兩個事件。相關參考視訊講解:進入學習

事件監聽

在React原始碼系列之二:React的渲染機制曾提到過,React在開始渲染前,會為應用建立一個fiberRoot作為應用的根節點。在建立fiberRoot還會做一件事,就是

listenToAllSupportedEvents(rootContainerElement);

從字面就能理解這個函數是做事件監聽的,其中rootContainerElement引數就是應用中的id = root的DOM元素。

該函數主要遍歷上面事件註冊新增到allNativeEvents的事件,按照一定規則,區分冒泡階段,捕獲階段,區分有無副作用進行監聽,監聽的api還是addEventListener:

// 監聽冒泡階段事件
function addEventBubbleListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, false);
  return listener;
}
// 監聽捕獲階段事件
function addEventCaptureListener(target, eventType, listener) {
  target.addEventListener(eventType, listener, true);
  return listener;
}

程式碼中的target就是id = root的DOM元素。

注意,上面監聽的listener是一個事件派發器,並不是真實的瀏覽器事件或你寫的事件回撥函數。 不要搞混淆了。

事件派發

上面提到,事件一旦在id = root的DOM元素中委託,其實是一直在觸發的,只是沒有繫結對應的回撥函數。

意思是,當我們把滑鼠移入我們的應用頁面中時,這時就在派發事件了,因為頁面的DOM元素是有監聽mousemove之類的事件的。

那問題來了,React是如何得知我們給事件繫結了回撥函數並觸發對應的回撥函數的?

帶著這個問題我們來研究下事件派發

要講事件派發,還得提下事件監聽階段監聽的listener,它實際是下面這玩意:

function createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags) {
  var eventPriority = getEventPriorityForPluginSystem(domEventName);
  var listenerWrapper;

  switch (eventPriority) {
    case DiscreteEvent:
      listenerWrapper = dispatchDiscreteEvent;
      break;

    case UserBlockingEvent:
      listenerWrapper = dispatchUserBlockingUpdate;
      break;

    case ContinuousEvent:
    default:
      listenerWrapper = dispatchEvent;
      break;
  }

  return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}

和事件註冊一樣,listener也分為dispatchDiscreteEvent, dispatchUserBlockingUpdate, dispatchEvent三種。它們之間的主要區別是執行優先順序,還有discreteEvent涉及到要清除之前的discreteEvent問題,所以做了區分。但是它們最後都會呼叫dispatchEvent

所以事件派發的角色應該是dispatchEvent

function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {

  var allowReplay = true;

  allowReplay = (eventSystemFlags & IS_CAPTURE_PHASE) === 0;
  // 如果有離散事件正在執行,會排隊,順序執行
  if (allowReplay && hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(domEventName)) {
    domEventName, eventSystemFlags, targetContainer, nativeEvent);
    return;
  }
  // 嘗試事件派發,如果成功,就不用執行下面的程式碼了
  var blockedOn = attemptToDispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent);
  // 嘗試事件派發成功
  if (blockedOn === null) {
    if (allowReplay) {
      // 清除連續事件佇列
      clearIfContinuousEvent(domEventName, nativeEvent);
    }

    return;
  }

  if (allowReplay) {
    if (isReplayableDiscreteEvent(domEventName)) {

      queueDiscreteEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent);
      return;
    }

    if (queueIfContinuousEvent(blockedOn, domEventName, eventSystemFlags, targetContainer, nativeEvent)) {
      return;
    } 

    clearIfContinuousEvent(domEventName, nativeEvent);
  } 

  dispatchEventForPluginEventSystem(domEventName, eventSystemFlags, nativeEvent, null, targetContainer);
}

介紹下dispatchEvent的幾個引數:

  • domEventName: DOM事件名稱,如:click,不是onClick
  • eventSystemFlags:事件系統標記;
  • targetContainerid=root的DOM元素;
  • nativeEvent:原生事件(來自addEventListener);

attemptToDispatchEvent中, 根據nativeEvent.target找到真正觸發事件的DOM元素,並根據DOM元素找到對應的fiber節點,判斷fiber節點的型別以及是否已渲染來決定是否要派發事件。

在一系列判斷通過後,就開始真正的事件處理了:

function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
  // 獲取觸發事件的DOM元素
  var nativeEventTarget = getEventTarget(nativeEvent);
  // 初始化事件派發佇列
  var dispatchQueue = [];
  // 合成事件
  extractEvents$5(dispatchQueue, domEventName, targetInst, nativeEvent, nativeEventTarget, eventSystemFlags);
  // 按事件派發佇列執行事件派發
  processDispatchQueue(dispatchQueue, eventSystemFlags);
}

extractEvents$5中會進行事件合成,放在下面單獨講。

processDispatchQueue會根據事件階段(冒泡或捕獲)來決定是正序還是倒序遍歷合成事件中的listeners

接下來就比較簡單了。 遍歷listeners執行上面的listener

合成事件

在合成事件中,會根據domEventName來決定使用哪種型別的合成事件。

click為例,當我們點選頁面的某個元素時,React會根據原生事件nativeEvent找到觸發事件的DOM元素和對應的fiber節點。並以該節點為孩子節點往上查詢,找到包括該節點及以上所有的click回撥函數建立dispatchListener,並新增到listeners陣列中。

// dispatchListener
{
    instance: instance,  // 事件所在的fiber節點
    listener: listener,  // 事件回撥函數
    currentTarget: currentTarget  // 事件對應的DOM元素
  }

當向上查詢完成後,會基於click型別的合成事件類建立事件

// 建立合成事件範例
var _event = new SyntheticEventCtor(reactName, reactEventType, null, nativeEvent, nativeEventTarget);
// 事件派發佇列新增事件
dispatchQueue.push({
  event: _event,   // 合成事件範例
  listeners: _listeners  // 同型別事件的集合陣列
});

看下SyntheticEventCtor

// Interface根據事件型別有所不同
function createSyntheticEvent(Interface) {
  // 合成事件建構函式
  function SyntheticBaseEvent(reactName, reactEventType, targetInst, nativeEvent, nativeEventTarget) {
    this._reactName = reactName;
    this._targetInst = targetInst;
    this.type = reactEventType;
    this.nativeEvent = nativeEvent;
    this.target = nativeEventTarget;
    this.currentTarget = null;
    // React根據不同事件型別寫了對應的屬性介面,這裡基於介面將原生事件上的屬性clone到建構函式中
    for (var _propName in Interface) {... }

    var defaultPrevented = nativeEvent.defaultPrevented != null ? nativeEvent.defaultPrevented : nativeEvent.returnValue === false;

    if (defaultPrevented) {
      this.isDefaultPrevented = functionThatReturnsTrue;
    } else {
      this.isDefaultPrevented = functionThatReturnsFalse;
    }

    this.isPropagationStopped = functionThatReturnsFalse;
    return this;
  }

  _assign(SyntheticBaseEvent.prototype, {
    // 阻止預設事件
    preventDefault: function () {...},
    // 阻止捕獲和冒泡階段中當前事件的進一步傳播
    stopPropagation: function () {...},
    // 合成事件不使用物件池了,這個事件是空的,沒有意義,儲存是為了向下相容不報錯。
    persist: function () {},

    isPersistent: functionThatReturnsTrue
  });

  return SyntheticBaseEvent;
}

看到這裡,我們基本能弄明白合成事件是個什麼東西了。

React合成事件是將同型別的事件找出來,基於這個型別的事件,React通過程式碼定義好的型別事件的介面和原生事件建立相應的合成事件範例,並重寫了preventDefaultstopPropagation方法。

這樣,同型別的事件會複用同一個合成事件範例物件,節省了單獨為每一個事件建立事件範例物件的開銷,這就是事件的合成。

捕獲和冒泡

事件派發分為兩個階段執行, 捕獲階段和冒泡階段。

在上面事件合成中講過,React會根據事件觸發的fiber節點向上查詢,將上面的同型別事件新增到佇列中,這樣天然就有了一個冒泡的順序,從最底層向上冒泡。如果倒序過來遍歷就是捕獲的順序。

所以,React實現冒泡和捕獲就很簡單了,只需要根據事件派發的階段,判斷是冒泡階段還是捕獲階段來決定是正序遍歷listeners還是倒序遍歷就行了。

總結

說是講React的合成事件,實際上講了React的事件系統。做下總結:

React的事件系統分為幾個部分:

1.事件註冊;

2.事件監聽;

3.事件合成;

4.事件派發; 事件系統流程:

  • React程式碼執行時,內部會自動執行事件的註冊;
  • 第一次渲染,建立fiberRoot時,會進行事件的監聽,所有的事件通過addEventListener委託在id=root的DOM元素上進行監聽;
  • 在我們觸發事件時,會進行事件合成,同型別事件複用一個合成事件類範例物件;
  • 最後進行事件的派發,執行我們程式碼中的事件回撥函數;

到此這篇關於深入分析React原始碼中的合成事件的文章就介紹到這了,更多相關React合成事件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com