Анализ исходного кода React — механизм событий

внешний интерфейс исходный код React.js
Анализ исходного кода React — механизм событий

Механизм событий React по-прежнему очень интересен, а идея симуляции доставки событий и использования document для делегирования большинства событий более интересна.

Блок-схема механизма событий

event-react

анализ кода

(код содержит только часть с параметрами события)

_updateDOMProperties — это точка входа для обработки параметров событий, просто обратите внимание на метод enqueuePutListener, который является функцией входа для регистрации событий. Переменная RegistrationNameModules содержит объект сопоставления типов событий и соответствующих методов, как показано на рисунке:

registrationnamemodules

Инициализация этих сопоставлений объясняется в разделе «Анализ исходного кода 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);
      }
    }
  }
}

enqueuePutListener

  • 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 Object Model Events), не все типы событий делегируются документу, а некоторые напрямую делегируются самому элементу.

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 для каждого элемента.

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 вызывает метод ExtractEvents соответствующего подключаемого модуля, чтобы получить события, которые необходимо выполнить для соответствующего типа подключаемого модуля, а затем объединить их вместе.

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;
 }

Интересная вещь о методе ExtractEvents плагина заключается в том, чтоEventPropagators.accumulateTwoPhaseDispatches(event).

EventPropagators.accumulateTwoPhaseDispatches имитирует процесс доставки события: захват -> цель -> пузырь, и возвращает все функции обратного вызова и соответствующие элементы, соответствующие типу события на этом пути, в порядке доставки события.

(картинка из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 имитирует процесс доставки события и получает соответствующую функцию обратного вызова и объект события и сохраняет их в _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);
  }
}

Прослушиватель запроса и соответствующий 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 появятся события, соответствующие сработавшему типу события, поместите все искусственные события в очередь событий через 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).
  • Почти все события делегируются документированию в целях оптимизации производительности.
  • При смешивании синтетических событий и нативных событий следует отметить, что события React в основном делегируются документам.

использованная литература