1. 程式人生 > >React原始碼分析之事件系統

React原始碼分析之事件系統

React原始碼分析之事件系統(轉載自阿里雲)

react自己實現了一套高效的事件系統,包括了事件的註冊、儲存、分發、和重用,在DOM事件體系基礎上做了很大改進,減少了記憶體消耗,簡化了事件邏輯,並最大化的解決了IE等瀏覽器的事件不相容問題。與傳統的DOM體系相比,它有如下特點:

  • 1、React元件上宣告的事件最終繫結到了document這個DOM節點上(事件代理),而不是React元件上的相應DOM節點。故只有document節點上綁定了原生的事件,其他的節點沒有繫結事件,減少了記憶體開銷。
  • 2、React以佇列的方式,從觸發事件的節點向父元件回溯,呼叫他們在JSX中宣告的callback。ue就是React自身實現了一套冒泡機制,我們沒有辦法使用e.stopPropagation()去阻止冒泡,應該使用e.preventDefault()。
  • 3、React有一套自己的合成事件SyntheticEvent,不同型別的事件會構造不同的SyntheticEvent。
  • 4、React使用物件池來管理合成事件的建立和銷燬,這樣減少了垃圾的生成和新物件記憶體的分配,大大提高了效能。
事件註冊

React中有一個_updateDOMProperties方法,該方法的作用是對JSX中元件宣告的屬性進行處理。

_updateDOMProperties: function (lastProps, nextProps, transaction) {
    ... // 前面程式碼太長,省略一部分
    else if (registrationNameModules.hasOwnProperty(propKey)) {
        // 如果是props這個物件直接宣告的屬性,而不是從原型鏈中繼承而來的,則處理它
        // nextProp表示要建立或者更新的屬性,而lastProp則表示上一次的屬性
        // 對於mountComponent,lastProp為null。updateComponent二者都不為null。unmountComponent則nextProp為null
        if (nextProp) {
          // mountComponent和updateComponent中,enqueuePutListener註冊事件
          enqueuePutListener(this, propKey, nextProp, transaction);
        } else if (lastProp) {
          // unmountComponent中,刪除註冊的listener,防止記憶體洩漏
          deleteListener(this, propKey);
        }
    }
}

當該方法發現到有個屬性是事件屬性時,呼叫enqueuePutListener方法對事件進行註冊。下面我們來看enqueuePutListener,它負責註冊JSX中宣告的事件。原始碼如下:

function enqueuePutListener(inst, registrationName, listener, transaction) {
  if (transaction instanceof ReactServerRenderingTransaction) {
    return;
  }
  var containerInfo = inst._hostContainerInfo;
  var isDocumentFragment = containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
  // 找到document
  var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
  // 註冊事件,將事件註冊到document上
  listenTo(registrationName, doc);
  // 儲存事件,放入事務佇列中
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
}

該方法會通過listenTo方法直接將事件註冊到document節點上,並使用putListener方法將相應的方法回撥進行儲存(在下面的事件儲存中將會看到),listenTo是由兩個方法trapBubbledEvent和listen組成的,下面看原始碼:

trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
    if (!element) {
      return null;
    }
    return EventListener.listen(
      element,   // 繫結到的DOM目標,也就是document
      handlerBaseName,   // eventType
      ReactEventListener.dispatchEvent.bind(null, topLevelType));  // callback, document上的原生事件觸發後回撥
  },

listen: function listen(target, eventType, callback) {
    if (target.addEventListener) {
      // 將原生事件新增到target這個dom上,也就是document上。
      // 這就是隻有document這個DOM節點上有原生事件的原因
      target.addEventListener(eventType, callback, false);
      return {
        // 刪除事件,這個由React自己回撥,不需要呼叫者來銷燬。但僅僅對於React合成事件才行
        remove: function remove() {
          target.removeEventListener(eventType, callback, false);
        }
      };
    } else if (target.attachEvent) {
      // attach和detach的方式
      target.attachEvent('on' + eventType, callback);
      return {
        remove: function remove() {
          target.detachEvent('on' + eventType, callback);
        }
      };
    }
  },

可以看到listen方法才是進行原生方法註冊的方法,值得注意的是document註冊所有的事件時的回撥為ReactEventListener.dispatchEvent,這個方法React中進行事件分發的方法,所有可以理解為document上註冊的事件被觸發後,React都會對該類事件進行事件的分發。

事件儲存

事件儲存由EventPluginHub來負責,它的入口在我們上面講到的enqueuePutListener中的putListener方法,如下

 /**
   * EventPluginHub用來儲存React事件, 將listener儲存到`listenerBank[registrationName][key]`
   *
   * @param {object} inst: 事件源
   * @param {string} listener的名字,比如onClick
   * @param {function} listener的callback
   */
  //
  putListener: function (inst, registrationName, listener) {

    // 用來標識註冊了事件,比如onClick的React物件。key的格式為'.nodeId', 只用知道它可以標示哪個React物件就可以了
    var key = getDictionaryKey(inst);
    var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
    // 將listener事件回撥方法存入listenerBank[registrationName][key]中,比如listenerBank['onclick'][nodeId]
    // 所有React元件物件定義的所有React事件都會儲存在listenerBank中
    bankForRegistrationName[key] = listener;

    //onSelect和onClick註冊了兩個事件回撥外掛, 用於walkAround某些瀏覽器相容bug,不用care
    var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
    if (PluginModule && PluginModule.didPutListener) {
      PluginModule.didPutListener(inst, registrationName, listener);
    }
  },

var getDictionaryKey = function (inst) {
  return '.' + inst._rootNodeID;
};

可以看到事件被儲存到了listenerBank當中,它按照事件名和React元件物件進行了二維劃分,如nodeId上註冊的onclick事件最後儲存在listenterBank.onClick[nodeId]上。

事件分發

在上面我們說到document上註冊的事件觸發的回撥是一樣的,都是使用dispatchEvent方法對該事件進行事件分發。dispatchEvent方法是事件分發的入口方法,下面我們就來看下dispatchEvent方法的實現。

// topLevelType:帶top的事件名,如topClick。不用糾結為什麼帶一個top欄位,知道它是事件名就OK了
// nativeEvent: 使用者觸發click等事件時,瀏覽器傳遞的原生事件
dispatchEvent: function (topLevelType, nativeEvent) {
    // disable了則直接不回撥相關方法
    if (!ReactEventListener._enabled) {
      return;
    }

    var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
    try {
      // 放入批處理佇列中,React事件流也是一個訊息佇列的方式
      ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
    } finally {
      TopLevelCallbackBookKeeping.release(bookKeeping);
    }
}

可見我們仍然使用批處理的方式進行事件分發,handleTopLevelImpl才是事件分發的真正執行者,它是事件分發的核心,體現了React事件分發的特點,如下:

// document進行事件分發,這樣具體的React元件才能得到響應。因為DOM事件是繫結到document上的
function handleTopLevelImpl(bookKeeping) {
  // 找到事件觸發的DOM和React Component
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);

  // 執行事件回撥前,先由當前元件向上遍歷它的所有父元件。得到ancestors這個陣列。
  // 因為事件回撥中可能會改變Virtual DOM結構,所以要先遍歷好元件層級
  var ancestor = targetInst;
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);

  // 從當前元件向父元件遍歷,依次執行註冊的回撥方法. 我們遍歷構造ancestors陣列時,是從當前元件向父元件回溯的,故此處事件回撥也是這個順序
  // 這個順序就是冒泡的順序,並且我們發現不能通過stopPropagation來阻止'冒泡'。
  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}

呼叫handleTopLevelImpl方法時,首先會去找到事件觸發的DOM元素,在找到DOM元素之後,會去回去該元素所有的祖先元素,將它們存在ancestors陣列中,然後遍歷ancestors陣列,執行它們上註冊的事件callback,從而實現了事件冒泡的功能。

事件callback的呼叫

事件callback的呼叫是通過_handleTopLevel方法完成,它其實是呼叫ReactBrowserEventEmitter.handleTopLevel() ,如下

// React事件呼叫的入口。DOM事件繫結在了document原生物件上,每次事件觸發,都會呼叫到handleTopLevel
  handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    // 採用物件池的方式構造出合成事件。不同的eventType的合成事件可能不同
    var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
    // 批處理佇列中的events
    runEventQueueInBatch(events);
  }

handleTopLevel方法是事件callback呼叫的核心。它主要做兩件事情,一方面利用瀏覽器回傳的原生事件構造出React合成事件,另一方面採用佇列的方式處理events。先看如何構造合成事件。

// 構造合成事件
  extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var events;
    // EventPluginHub可以儲存React合成事件的callback,也儲存了一些plugin,這些plugin在EventPluginHub初始化時就註冊就來了
    var plugins = EventPluginRegistry.plugins;
    for (var i = 0; i < plugins.length; i++) {
      var possiblePlugin = plugins[i];
      if (possiblePlugin) {
        // 根據eventType構造不同的合成事件SyntheticEvent
        var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
        if (extractedEvents) {
          // 將構造好的合成事件extractedEvents新增到events陣列中,這樣就儲存了所有plugin構造的合成事件
          events = accumulateInto(events, extractedEvents);
        }
      }
    }
    return events;
  },

EventPluginRegistry.plugins預設包含五種plugin,他們是在EventPluginHub初始化階段注入進去的,且看程式碼

  // 將eventPlugin註冊到EventPluginHub中
  ReactInjection.EventPluginHub.injectEventPluginsByName({
    SimpleEventPlugin: SimpleEventPlugin,
    EnterLeaveEventPlugin: EnterLeaveEventPlugin,
    ChangeEventPlugin: ChangeEventPlugin,
    SelectEventPlugin: SelectEventPlugin,
    BeforeInputEventPlugin: BeforeInputEventPlugin
  });

不同的plugin針對不同的事件有特殊的處理,此處我們不展開講了,下面僅分析SimpleEventPlugin中方法即可。

我們先看SimpleEventPlugin如何構造它所對應的React合成事件。

  // 根據不同事件型別,比如click,focus構造不同的合成事件SyntheticEvent, 如SyntheticKeyboardEvent SyntheticFocusEvent
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
    var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
    if (!dispatchConfig) {
      return null;
    }
    var EventConstructor;

   // 根據事件型別,採用不同的SyntheticEvent來構造不同的合成事件
    switch (topLevelType) {
      ... // 省略一些事件,我們僅以blur和focus為例
      case 'topBlur':
      case 'topFocus':
        EventConstructor = SyntheticFocusEvent;
        break;
      ... // 省略一些事件
    }

    // 從event物件池中取出合成事件物件,利用物件池思想,可以大大降低物件建立和銷燬的時間,提高效能。這是React事件系統的一大亮點
    var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
    EventPropagators.accumulateTwoPhaseDispatches(event);
    return event;
},

這裡我們看到了event物件池這個重大特性,採用合成事件物件池的方式,可以大大降低銷燬和建立合成事件帶來的效能開銷。

物件建立好之後,我們還會將它新增到events這個佇列中,因為事件回撥的時候會用到這個佇列。新增到events中使用的是accumulateInto方法。它思路比較簡單,將新建立的合成物件的引用新增到之前建立好的events佇列中即可,原始碼如下

function accumulateInto(current, next) {

  if (current == null) {
    return next;
  }

  // 將next新增到current中,返回一個包含他們兩個的新陣列
  // 如果next是陣列,current不是陣列,採用push方法,否則採用concat方法
  // 如果next不是陣列,則返回一個current和next構成的新陣列
  if (Array.isArray(current)) {
    if (Array.isArray(next)) {
      current.push.apply(current, next);
      return current;
    }
    current.push(next);
    return current;
  }

  if (Array.isArray(next)) {
    return [current].concat(next);
  }

  return [current, next];
}
批處理合成事件

我們上面分析過了,React以佇列的形式處理合成事件。方法入口為runEventQueueInBatch,如下

  function runEventQueueInBatch(events) {
    // 先將events事件放入佇列中
    EventPluginHub.enqueueEvents(events);
    // 再處理佇列中的事件,包括之前未處理完的。先入先處理原則
    EventPluginHub.processEventQueue(false);
  }

  /**
   * syntheticEvent放入佇列中,等到processEventQueue再獲得執行
   */
  enqueueEvents: function (events) {
    if (events) {
      eventQueue = accumulateInto(eventQueue, events);
    }
  },

  /**
   * 分發執行佇列中的React合成事件。React事件是採用訊息佇列方式批處理的
   *
   * simulated:為true表示React測試程式碼,我們一般都是false 
   */
  processEventQueue: function (simulated) {
    // 先將eventQueue重置為空
    var processingEventQueue = eventQueue;
    eventQueue = null;
    if (simulated) {
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseSimulated);
    } else {
      // 遍歷處理佇列中的事件,
      // 如果只有一個元素,則直接executeDispatchesAndReleaseTopLevel(processingEventQueue)
      // 否則遍歷佇列中事件,呼叫executeDispatchesAndReleaseTopLevel處理每個元素
      forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
    }
    // This would be a good time to rethrow if any of the event handlers threw.
    ReactErrorUtils.rethrowCaughtError();
  },

合成事件處理也分為兩步,先將我們要處理的events佇列放入eventQueue中,因為之前可能就存在還沒處理完的合成事件。然後再執行eventQueue中的事件。可見,如果之前有事件未處理完,這裡就又有得到執行的機會了。

事件執行的入口方法為executeDispatchesAndReleaseTopLevel,如下

var executeDispatchesAndReleaseTopLevel = function (e) {
  return executeDispatchesAndRelease(e, false);
};

var executeDispatchesAndRelease = function (event, simulated) {
  if (event) {
    // 進行事件分發,
    EventPluginUtils.executeDispatchesInOrder(event, simulated);

    if (!event.isPersistent()) {
      // 處理完,則release掉event物件,採用物件池方式,減少GC
      // React幫我們處理了合成事件的回收機制,不需要我們關心。但要注意,如果使用了DOM原生事件,則要自己回收
      event.constructor.release(event);
    }
  }
};

// 事件處理的核心
function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;

  if (Array.isArray(dispatchListeners)) {
    // 如果有多個listener,則遍歷執行陣列中event
    for (var i = 0; i < dispatchListeners.length; i++) {
      // 如果isPropagationStopped設成true了,則停止事件傳播,退出迴圈。
      if (event.isPropagationStopped()) {
        break;
      }
      // 執行event的分發,從當前觸發事件元素向父元素遍歷
      // event為瀏覽器上傳的原生事件
      // dispatchListeners[i]為JSX中宣告的事件callback
      // dispatchInstances[i]為對應的React Component 
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    // 如果只有一個listener,則直接執行事件分發
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  // 處理完event,重置變數。因為使用的物件池,故必須重置,這樣才能被別人複用
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

executeDispatchesInOrder會先得到event對應的listeners佇列,然後從當前元素向父元素遍歷執行註冊的callback。且看executeDispatch

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    // test程式碼使用,支援try-catch,其他就沒啥區別了
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    // 事件分發,listener為callback,event為引數,類似listener(event)這個方法呼叫
    // 這樣就回調到了我們在JSX中註冊的callback。比如onClick={(event) => {console.log(1)}}
    // 這樣應該就明白了callback怎麼被呼叫的,以及event引數怎麼傳入callback裡面的了
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

// 採用func(a)的方式進行呼叫,
// 故ReactErrorUtils.invokeGuardedCallback(type, listener, event)最終呼叫的是listener(event)
// event物件為瀏覽器傳遞的DOM原生事件物件,這也就解釋了為什麼React合成事件回撥中能拿到原生event的原因
function invokeGuardedCallback(name, func, a) {
  try {
    func(a);
  } catch (x) {
    if (caughtError === null) {
      caughtError = x;
    }
  }
}