React源碼分析 - 事件機制

React的事件機制仍是很好玩的,其中模擬事件傳遞和利用document委託大部分事件的想法比較有意思。javascript

事件機制流程圖

event-react

代碼分析

(代碼僅包含涉及事件參數的部分)html

_updateDOMProperties是事件參數處理的入口,只要注意enqueuePutListener這個方法就行了,這是註冊事件的入口函數。registrationNameModules變量保存事件類型和對應的方法的映射的一個對象,如圖:java

registrationnamemodules

這些映射的初始化的地方在《React源碼分析 - 組件初次渲染》解釋過了。react

_updateDOMProperties: function (lastProps, nextProps, transaction) {
  var propKey;
  var styleName;
  var styleUpdates;
  for (propKey in lastProps) {
    if (nextProps.hasOwnProperty(propKey) || !lastProps.hasOwnProperty(propKey) || lastProps[propKey] == null) {
      continue;
    }
    if (registrationNameModules.hasOwnProperty(propKey)) {
      if (lastProps[propKey]) {
        // Only call deleteListener if there was a listener previously or
        // else willDeleteListener gets called when there wasn't actually a
        // listener (e.g., onClick={null})
        deleteListener(this, propKey);
      }
    }
  }
  for (propKey in nextProps) {
    var nextProp = nextProps[propKey];
    var lastProp = propKey === STYLE ? this._previousStyleCopy : lastProps != null ? lastProps[propKey] : undefined;
    if (!nextProps.hasOwnProperty(propKey) || nextProp === lastProp || nextProp == null && lastProp == null) {
      continue;
    }
    if (registrationNameModules.hasOwnProperty(propKey)) { // 處理事件參數。
      if (nextProp) {
        enqueuePutListener(this, propKey, nextProp, transaction); // 註冊事件,委託到屬於的document上
      } else if (lastProp) {
        deleteListener(this, propKey);
      }
    }
  }
}
複製代碼

enqueuePutListenergit

  • listenTo
  • putListener
function enqueuePutListener(inst, registrationName, listener, transaction) {
  var containerInfo = inst._nativeContainerInfo;
  var doc = containerInfo._ownerDocument; // 大部分的事件都被到對應的document上
  if (!doc) { // ssr
    // Server rendering.
    return;
  }
  listenTo(registrationName, doc);
  transaction.getReactMountReady().enqueue(putListener, {
    inst: inst,
    registrationName: registrationName,
    listener: listener
  });
}
複製代碼

listenTo是將事件委託到document的方法,大部分事件是委託到document上的。可是由於document上可以catch的事件類型的限制(Document Object Model Events),不是全部的事件類型都委託到document,少部分是直接委託到元素自己上的。github

putListener將對應的類型的事件、事件的目標對象和事件觸發時執行的方法添加到listenerBank對象中。數組

listenTo: function (registrationName, contentDocumentHandle) {
  var mountAt = contentDocumentHandle;
  var isListening = getListeningForDocument(mountAt);
  var dependencies = EventPluginRegistry.registrationNameDependencies[registrationName];

  var topLevelTypes = EventConstants.topLevelTypes;
  for (var i = 0; i < dependencies.length; i++) {
    var dependency = dependencies[i];
    if (!(isListening.hasOwnProperty(dependency) && isListening[dependency])) {
      // 先判斷先幾個須要特殊處理的事件,主要都是兼容性的緣由。
      if (...) {
        ......
      } else if (topEventMapping.hasOwnProperty(dependency)) {
        ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
      }
      isListening[dependency] = true;
    }
  }
}

// 冒泡階段的觸發的事件的委託
trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
  return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(topLevelType, handlerBaseName, handle);
},

// 捕獲階段的觸發的事件的委託
trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
  return ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(topLevelType, handlerBaseName, handle);
},

trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
  return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},

trapCapturedEvent: function (topLevelType, handlerBaseName, handle) {
  return EventListener.capture(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},

listen: function listen(target, eventType, callback) {
  if (target.addEventListener) {
    target.addEventListener(eventType, callback, false);
  }
},

capture: function capture(target, eventType, callback) {
  if (target.addEventListener) {
    target.addEventListener(eventType, callback, true);
  }
},
複製代碼

重點在於全部的委託的事件的回調函數都是ReactEventListener.dispatchEvent。性能優化

dispatchEvent: function (topLevelType, nativeEvent) {
  // bookKeeping的初始化使用了react在源碼中用到的對象池的方法來避免多餘的垃圾回收。
  // bookKeeping的做用看ta的定義就知道了,就是一個用來保存過程當中會使用到的變量的對象。
  var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
  try {
    ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
  } finally {
    TopLevelCallbackBookKeeping.release(bookKeeping);
  }
}
複製代碼

handleTopLevelImpl方法遍歷事件觸發對象以及其的父級元素(事件傳遞),對每一個元素執行_handleTopLevel方法。app

function handleTopLevelImpl(bookKeeping) {
  var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
  var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
  var ancestor = targetInst;
  do {
    bookKeeping.ancestors.push(ancestor);
    ancestor = ancestor && findParent(ancestor);
  } while (ancestor);

  for (var i = 0; i < bookKeeping.ancestors.length; i++) {
    targetInst = bookKeeping.ancestors[i];
    ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
  }
}
複製代碼

handleTopLevel根據事件對象以及觸發的事件類型提取出全部須要被執行的事件以及對應的回調函數,統一由runEventQueueInBatch執行。函數

handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
  var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
  runEventQueueInBatch(events);
}
複製代碼

extractEvents方法調用了對應的plugin的extractEvents方法來獲取對應的plugin類型的須要執行的事件,而後accumulateInto到一塊兒。

extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
   var events;
   var plugins = EventPluginRegistry.plugins;
   for (var i = 0; i < plugins.length; i++) {
     // Not every plugin in the ordering may be loaded at runtime.
     var possiblePlugin = plugins[i];
     if (possiblePlugin) {
       var extractedEvents = possiblePlugin.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
       if (extractedEvents) {
         events = accumulateInto(events, extractedEvents);
       }
     }
   }
   return events;
 }
複製代碼

plugin的extractEvents方法中的有意思的地方在於 EventPropagators.accumulateTwoPhaseDispatches(event)

EventPropagators.accumulateTwoPhaseDispatches中模擬了事件傳遞的過程即:capture -> target -> bubble 的過程,將這個路徑上的全部的符合事件類型的回調函數以及對應的元素按照事件傳遞的順序返回。

(圖片來自Event dispatch and DOM event flow

function traverseTwoPhase(inst, fn, arg) {
  var path = [];
  while (inst) {
    path.push(inst);
    inst = inst._nativeParent;
  }
  var i;
  for (i = path.length; i-- > 0;) {
    fn(path[i], false, arg);
  }
  for (i = 0; i < path.length; i++) {
    fn(path[i], true, arg);
  }
}
複製代碼

traverseTwoPhase方法模擬了事件傳遞的過程而且獲取對應的回調函數和事件對象保存在react合成的event對象的_dispatchListeners和_dispatchInstances上

function accumulateDirectionalDispatches(inst, upwards, event) {
  var phase = upwards ? PropagationPhases.bubbled : PropagationPhases.captured;
  var listener = listenerAtPhase(inst, event, phase);
  if (listener) {
    // event._dispatchListeners結果就是這個event在event flow的過程當中會觸發那些listenter的callback【按照event flow的順序push到一個數組中了】
    event._dispatchListeners = accumulateInto(event._dispatchListeners, listener);
    event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
  }
}
複製代碼

查詢listener和對應的inst使用的是事件的類型以及_rootNodeID,listenerBank中保存了對應一個類型下元素的回調函數:

listenerBank

function listenerAtPhase(inst, event, propagationPhase) {
  var registrationName = event.dispatchConfig.phasedRegistrationNames[propagationPhase];
  return getListener(inst, registrationName);
}

getListener: function (inst, registrationName) {
    var bankForRegistrationName = listenerBank[registrationName];
    return bankForRegistrationName && bankForRegistrationName[inst._rootNodeID];
  },
複製代碼

對於listenerBank內容的生成由以前說的第二個主要方法putListener完成。

putListener 使用事務的方式統一在ReactMountReady階段執行。

putListener: function (inst, registrationName, listener) {
  var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
  bankForRegistrationName[inst._rootNodeID] = listener;
}
複製代碼

在extractEvents了對應觸發的事件類型的events後經過runEventQueueInBatch(events)將全部的合成事件放到事件隊列裏面,第二步是逐個執行

function runEventQueueInBatch(events) {
  EventPluginHub.enqueueEvents(events);
  EventPluginHub.processEventQueue(false);
}

function executeDispatchesInOrder(event, simulated) {
  var dispatchListeners = event._dispatchListeners;
  var dispatchInstances = event._dispatchInstances;
  if (process.env.NODE_ENV !== 'production') {
    validateEventDispatches(event);
  }
  if (Array.isArray(dispatchListeners)) {
    for (var i = 0; i < dispatchListeners.length; i++) {
      if (event.isPropagationStopped()) {
        break;
      }
      // Listeners and Instances are two parallel arrays that are always in sync.
      executeDispatch(event, simulated, dispatchListeners[i], dispatchInstances[i]);
    }
  } else if (dispatchListeners) {
    executeDispatch(event, simulated, dispatchListeners, dispatchInstances);
  }
  event._dispatchListeners = null;
  event._dispatchInstances = null;
}

function executeDispatch(event, simulated, listener, inst) {
  var type = event.type || 'unknown-event';
  event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
  if (simulated) {
    ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
  } else {
    ReactErrorUtils.invokeGuardedCallback(type, listener, event);
  }
  event.currentTarget = null;
}

function invokeGuardedCallback(name, func, a, b) {
  try {
    return func(a, b);
  } catch (x) {
    if (caughtError === null) {
      caughtError = x;
    }
    return undefined;
  }
}
複製代碼

總結

  • 統一的分發函數dispatchEvent。
  • React的事件對象是合成對象(SyntheticEvent)。
  • 幾乎全部的事件都委託到document,達到性能優化的目的。
  • 合成事件與原生事件混用要注意React的事件基本都是委託到document。

參考資料

相關文章
相關標籤/搜索