返回

React事件系统剖析与源码解读

前端

React事件系统概览

React事件系统是一个基于发布-订阅模式的事件处理系统。当用户在React组件上触发事件时,React事件系统会将该事件发布出去,然后由订阅了该事件的组件进行处理。React事件系统提供了丰富的事件类型,涵盖了常见的用户交互操作,例如点击、鼠标移动、键盘输入等。

React事件系统的核心设计原则是事件委托、事件合成和事件冒泡/捕获。

  • 事件委托: React事件系统采用事件委托的机制来处理事件。当用户在React组件上触发事件时,事件会首先由最外层的组件捕获到,然后逐层向内传递,直到找到最合适的组件来处理该事件。这种事件处理方式可以减少事件处理器的数量,提高事件处理的效率。

  • 事件合成: React事件系统采用事件合成的机制来处理事件。当用户在React组件上触发事件时,React事件系统会将该事件合成一个标准的事件对象,然后将该事件对象传递给订阅了该事件的组件。这种事件处理方式可以使事件处理更加一致,也更容易维护。

  • 事件冒泡/捕获: React事件系统支持事件冒泡和事件捕获两种事件处理机制。事件冒泡是指事件从最外层的组件向内层组件逐层传递的过程,而事件捕获是指事件从最内层的组件向外层组件逐层传递的过程。React事件系统允许开发者通过事件监听器来指定事件的处理方式,例如,可以使用addEventListener()方法来监听事件冒泡,也可以使用attachEvent()方法来监听事件捕获。

React事件系统源码分析

React事件系统的源码位于react-dom包中,主要包含以下几个模块:

  • EventPluginRegistry模块:该模块负责管理所有事件插件,并提供获取和注册事件插件的接口。
  • EventPluginHub模块:该模块负责协调事件插件之间的通信,并为事件插件提供统一的事件处理接口。
  • EventPluginUtils模块:该模块提供了一些与事件插件相关的实用工具函数。
  • SyntheticEvent模块:该模块定义了合成事件对象的结构和行为。
  • EventTarget模块:该模块定义了事件目标对象的结构和行为。

下面我们将对React事件系统的源码进行详细的分析,帮助读者深入理解React事件系统的运作原理。

EventPluginRegistry模块

EventPluginRegistry模块位于react-dom/src/events/EventPluginRegistry.js文件中,该模块负责管理所有事件插件,并提供获取和注册事件插件的接口。

import { invariant } from 'shared/invariant';

/**
 * Injectable ordering of event plugins.
 */
const eventPluginOrder = [];

/**
 * Plugins that are injected/picked up during runtime.
 */
const injectedEventPluginOrder = [];

/**
 * Ordered list of injected plugins.
 */
const orderedInjectedEventPluginOrder = [];

/**
 * PLUGIN_EVENT_TYPE_RELATIONS:
 *
 * */
const pluginEventTypes = {};

/**
 * Adding event plugin dependency relationship means if A depends on B,
 * B should come before A in the event plugin ordering.
 *
 * */
const eventPluginDependencies = {};

/**
 * Mapping from event types to plugin names.
 * We can warn in case of missing plugin or in case of extra plugin.
 */
const eventTypesToPluginNames = {};

function addEventPluginDependency(pluginName, eventName) {
  invariant(
    pluginName !== 'react-initialize-touch-event' &&
      pluginName !== 'react-touch-events',
    'This event plugin system is not compatible with `react-initialize-touch-event` and `react-touch-events` plugins.',
  );
  pluginEventTypes[eventName] = pluginName;
  eventPluginDependencies[pluginName] =
    eventPluginDependencies[pluginName] || [];
  eventPluginDependencies[pluginName].push(eventName);
}

/**
 * In some cases, certain plugins should be injected before other plugins.
 * This method provides a way to order plugins to ensure the correct ordering.
 *
 * @see {EventPluginRegistry.addPlugin()}
 */
function injectEventPluginOrder(injectedEventPluginName) {
  injectedEventPluginOrder.push(injectedEventPluginName);

  // Ensure injected plugins are placed at the top of the order.
  if (enableStableConcurrentModeAPIs) {
    const pluginName = injectedEventPluginName;
    const pluginIndex = pluginEventTypes[pluginName];
    if (pluginIndex) {
      eventPluginOrder.splice(
        pluginIndex,
        0,
        injectedEventPluginName,
      );
      orderedInjectedEventPluginOrder.push(injectedEventPluginName);
    }
  }
}

/**
 * Resolves a dependency graph that describes the order in which plugins
 * should be injected.
 *
 * @param {array} dependentPluginNames - event plugin names that want to be
 * updated.
 * @param {array} pluginDependencies - mapping from plugin names to the plugin
 * names that it depends on.
 *
 * @returns {array} plugin names in the order in which they should be injected.
 */
function orderPluginDependencies(dependentPluginNames, pluginDependencies) {
  let danglingDependencies = new Set();
  let pluginOrder = [];
  dependentPluginNames.forEach(function(pluginName) {
    if (!pluginDependencies.hasOwnProperty(pluginName)) {
      return;
    }
    const pluginDependenciesForThisPlugin = pluginDependencies[pluginName];
    pluginDependenciesForThisPlugin.forEach(function(dependencyName) {
      if (!pluginOrder.includes(dependencyName)) {
        const danglingDependency = {
          dependencyName,
          pluginName,
        };
        danglingDependencies.add(danglingDependency);
      }
    });
    if (!pluginOrder.includes(pluginName)) {
      pluginOrder.push(pluginName);
    }
  });

  while (danglingDependencies.size) {
    const danglingDependency = danglingDependencies.values().next().value;
    danglingDependencies.delete(danglingDependency);
    const pluginName = danglingDependency.pluginName;
    const dependencyName = danglingDependency.dependencyName;
    const pluginDependenciesForThisPlugin = pluginDependencies[pluginName];
    pluginDependenciesForThisPlugin.forEach(function(dependencyName) {
      const index = pluginOrder.indexOf(dependencyName);
      if (index > -1) {
        pluginOrder.splice(index + 1, 0, pluginName);
        return;
      }
    });
  }

  return pluginOrder;
}

/**
 * Finds plugin names that are required to be injected in order for a given
 * event.
 *
 * @param {string} eventName - name of the event.
 *
 * @returns {array} plugin names that are required to be injected.
 */
function getRequiredPluginNamesForEvent(eventName) {
  const pluginName = pluginEventTypes[eventName];
  if (!pluginName) {
    return [];
  }
  const eventPluginDependenciesForThisPlugin =
    eventPluginDependencies[pluginName];
  if (!eventPluginDependenciesForThisPlugin) {
    return [pluginName];
  }
  return eventPluginDependenciesForThisPlugin.map(getRequiredPluginNamesForEvent)
    .concat([pluginName]);
}

/**
 * Injects an event plugin.
 *
 * @param {object} plugin - the plugin that should be injected.
 * @param {string} name - name of the plugin.
 */
function injectEventPlugin(plugin, name) {
  const pluginIndex = eventPluginOrder.indexOf(name);
  if (pluginIndex > -1) {
    eventPluginOrder.splice(pluginIndex, 1);
  }
  eventPluginOrder.push(name);
  injectedEventPluginOrder.push(name);

  // Link events with the plugin.
  for (let eventName in plugin) {
    const discreteEventName =
      eventName.toLowerCase();
    const domEventName = topLevelEventsToDOM[discreteEventName];
    if (domEventName) {
      addEventPluginDependency(name, domEventName);
      eventTypesToPluginNames[domEventName] = name;
    }
  }
}

export {
  getRequiredPluginNamesForEvent,
  injectEventPluginOrder,
  injectEventPlugin,
};

EventPluginHub模块