React事件系统剖析与源码解读
2023-10-19 09:04:49
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,
};