react有一套复杂的事件机制,一句话总结react在web环境下的事件机制就是运用事件代理让document担当事件监听的职务, 当事件触发时找到react组件中绑定的所有事件回调函数并依次执行。那么,react是怎么在document上设置对应的事件监听函数,是怎么在事件触发时执行绑定的各种回调函数的呢?这篇文章就是带大家把react在web上的事件机制的代码细细捋一遍。(本文章以react 15.6.0的代码为准)
react源码中有官方示例图介绍react的事件机制,虽然图中介绍过于简单难以让读者了解react事件机制的详情,但是也可以大体看一下:
/**
* Summary of `ReactBrowserEventEmitter` event handling:
*
* - Top-level delegation is used to trap most native browser events. This
* may only occur in the main thread and is the responsibility of
* ReactEventListener, which is injected and can therefore support pluggable
* event sources. This is the only work that occurs in the main thread.
*
* - We normalize and de-duplicate events to account for browser quirks. This
* may be done in the worker thread.
*
* - Forward these native events (with the associated top-level type used to
* trap it) to `EventPluginHub`, which in turn will ask plugins if they want
* to extract any synthetic events.
*
* - The `EventPluginHub` will then process each event by annotating them with
* "dispatches", a sequence of listeners and IDs that care about that event.
*
* - The `EventPluginHub` then dispatches the events.
*
* Overview of React and the event system:
*
* +------------+ .
* | DOM | .
* +------------+ .
* | .
* v .
* +------------+ .
* | ReactEvent | .
* | Listener | .
* +------------+ . +-----------+
* | . +--------+|SimpleEvent|
* | . | |Plugin |
* +-----|------+ . v +-----------+
* | | | . +--------------+ +------------+
* | +-----------.--->|EventPluginHub| | Event |
* | | . | | +-----------+ | Propagators|
* | ReactEvent | . | | |TapEvent | |------------|
* | Emitter | . | |<---+|Plugin | |other plugin|
* | | . | | +-----------+ | utilities |
* | +-----------.--->| | +------------+
* | | | . +--------------+
* +-----|------+ . ^ +-----------+
* | . | |Enter/Leave|
* + . +-------+|Plugin |
* +-------------+ . +-----------+
* | application | .
* |-------------| .
* | | .
* | | .
* +-------------+ .
* .
* React Core . General Purpose Event Plugin System
*/
我把react事件机制分为三个阶段:
第一个阶段是为react事件机制做准备工作、为重要的功能模块注册默认配置和方法的准备阶段,具体做三个工作,一是注册ReactBrowserEventEmitter模块的ReactEventListener(ReactEventListener顾名思义就是用于react事件监听的模块); 二是注册EventPluginUtils的ComponentTree和TreeTraversal;三是生成EventPluginRegistry(顾名思义就是用于管理事件插件的模块)模块的各种数据。这一阶段发生于react工程引入ReactDOM时。
第二个阶段是事件注册阶段,主要做两件事情,一个是为document绑定上对应事件的回调函数,一个是把react事件回调函数存到EventPluginHub中。这一阶段发生于绑定事件处理函数的react元素挂载之前。
第三个阶段是执行事件回调函数的事件触发阶段;先是生成合成事件对象,然后依次执行react事件回调函数。这一阶段发生于浏览器事件发生后。
(阅读提醒:由于react事件机制的代码相当复杂(原因包括事件机制本身相当复杂、react要兼容web和native两种平台的事件机制、react源码使用es5和cjs并且模块划分过细、react中应用了大量根据各种设计模式包装的工具等),因此你在阅读代码过程经常会突然发现自己正在读的代码和自己已弄明白的部分断线了,不知道正在读的代码是干什么的这种情况。因此大家在读react源码时一定要带着目的读,时刻提醒自己现在读的是什么功能的代码,在失去目的之前把自己拉回来。再就是读之前要先把react源码中经常用的工具和设计模式如Transaction,PooledClass等搞明白再去阅读。)
一,react事件机制--准备阶段
准备阶段的代码非常简单,但是对于理解react事件机制是非常的重要的。
准备阶段的入口在src/renders/dom/ReactDOM.js文件中,ReactDOM.js会在导出ReactDOM对象之前执行ReactDefaultInjection.inject()开始为事件机制做一些重要的准备工作。也就是说在react工程引入ReactDom后,react事件机制的准备工作就做好了。ReactDefaultInjection.inject函数做的工作很多,事件相关的代码如下(src/renders/dom/shared/ReactDefaultInjection.js):
ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
/**
* Inject modules for resolving DOM hierarchy and plugin ordering.
*/
ReactInjection.EventPluginHub.injectEventPluginOrder(DefaultEventPluginOrder);
ReactInjection.EventPluginUtils.injectComponentTree(ReactDOMComponentTree);
ReactInjection.EventPluginUtils.injectTreeTraversal(ReactDOMTreeTraversal);
/**
* Some important event plugins included by default (without having to require
* them).
*/
ReactInjection.EventPluginHub.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
1、ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener)
ReactInjection.EventEmitter是ReactBrowerEventEmitter模块(src/renders/dom/client/ReactBrowerEventEmitter.js),执行injectReactEventListener(ReactEventListener)后会把ReactEventListener保存下来以供后续监听事件时使用;同时把自己的handleTopLevel函数设置为ReactEventListener的handleTopLevel函数,以供事件触发时使用。相关代码如下:
injectReactEventListener: function(ReactEventListener) {
ReactEventListener.setHandleTopLevel(
ReactBrowserEventEmitter.handleTopLevel,
);
ReactBrowserEventEmitter.ReactEventListener = ReactEventListener;
},
2、EventPluginUtils.injectComponentTree和EventPluginUtils.injectTreeTraversal
这两个函数分别是EventPluginUtils模块的injectComponentTree和injectTreeTraversal函数,分别把ReactDOMComponentTree和ReactDOMTreeTraversal保存为EventPluginUtils模块的ComponentTree和TreeTraversal以供后面构建合成事件对象用。代码如下(src/renderers/shared/stack/event/EventPluginUtils.js):
/**
* Injected dependencies:
*/
/**
* - `ComponentTree`: [required] Module that can convert between React instances
* and actual node references.
*/
var ComponentTree;
var TreeTraversal;
var injection = {
injectComponentTree: function(Injected) {
ComponentTree = Injected;
},
injectTreeTraversal: function(Injected) {
TreeTraversal = Injected;
},
};
3、EventPluginHub.injectEventPluginOrder 和EventPluginHub.injectEventPluginsByName
react事件对象是合成事件对象,react用各种事件插件生成合成事件对象的,又用EventPluginRegistry模块管理事件插件。EventPluginHub.injectEventPluginsByName和injectEventPluginOrder实际上是调用的EventPluginRegistry模块的同名函数。执行完毕后,会把SimpleEventPlugin、EnterLeaveEventPlugin等五种事件插件注册到EventPluginRegistry中的namesToPlugins对象上,并且生成如下数据:
/**
* Injectable ordering of event plugins.
*/
var eventPluginOrder: EventPluginOrder = null;
/**
* Ordered list of injected plugins.
*/
plugins: [],
/**
* Mapping from event name to dispatch config
*/
eventNameDispatchConfigs: {},
/**
* Mapping from registration name to plugin module
*/
registrationNameModules: {},
/**
* Mapping from registration name to event name
*/
registrationNameDependencies: {},
结果就是EventPluginRegistry可以方便的通过事件的name、registration name等信息获取事件及其依赖事件对应的事件插件;具体代码如下(src/renders/shared/stack/event/EventPluginRegistry.js):
injectEventPluginOrder: function(
injectedEventPluginOrder: EventPluginOrder,
): void {
eventPluginOrder = Array.prototype.slice.call(injectedEventPluginOrder);
recomputePluginOrdering();
},
injectEventPluginsByName: function(
injectedNamesToPlugins: NamesToPlugins,
): void {
var isOrderingDirty = false;
for (var pluginName in injectedNamesToPlugins) {
if (!injectedNamesToPlugins.hasOwnProperty(pluginName)) {
continue;
}
var pluginModule = injectedNamesToPlugins[pluginName];
if (
!namesToPlugins.hasOwnProperty(pluginName) ||
namesToPlugins[pluginName] !== pluginModule
) {
namesToPlugins[pluginName] = pluginModule;
isOrderingDirty = true;
}
}
if (isOrderingDirty) {
recomputePluginOrdering();
}
},
为了理解publishEventForPlugin函数和以后的publishRegistrationName函数,先看一下事件插件的代码:
var eventTypes = {
change: {
phasedRegistrationNames: {
bubbled: 'onChange',
captured: 'onChangeCapture',
},
dependencies: [
'topBlur',
'topChange',
'topClick',
'topFocus',
'topInput',
'topKeyDown',
'topKeyUp',
'topSelectionChange',
],
},
};
/**
* This plugin creates an `onChange` event that normalizes change events
* across form elements. This event fires at a time when it's possible to
* change the element's value without seeing a flicker.
*
* Supported elements are:
* - input (see `isTextInputElement`)
* - textarea
* - select
*/
var ChangeEventPlugin = {
eventTypes: eventTypes,
_allowSimulatedPassThrough: true,
_isInputEventSupported: isInputEventSupported,
// other codes
}
recomputePluginOrdering函数如下,对每一个事件插件按eventPluginOrder插入plugins函数并调用publishEventForPlugin:
/**
* Publishes an event so that it can be dispatched by the supplied plugin.
*
* @param {object} dispatchConfig Dispatch configuration for the event.
* @param {object} PluginModule Plugin publishing the event.
* @return {boolean} True if the event was successfully published.
* @private
*/
function recomputePluginOrdering(): void {
if (!eventPluginOrder) {
// Wait until an `eventPluginOrder` is injected.
return;
}
for (var pluginName in namesToPlugins) {
var pluginModule = namesToPlugins[pluginName];
var pluginIndex = eventPluginOrder.indexOf(pluginName);
if (EventPluginRegistry.plugins[pluginIndex]) {
continue;
}
EventPluginRegistry.plugins[pluginIndex] = pluginModule;
var publishedEvents = pluginModule.eventTypes;
for (var eventName in publishedEvents) {
invariant(
publishEventForPlugin(
publishedEvents[eventName],
pluginModule,
eventName,
),
'EventPluginRegistry: Failed to publish event `%s` for plugin `%s`.',
eventName,
pluginName,
);
}
}
}
publishRegistrationName函数如下,生成registrationNameModules和registrationNameDependencies:
/**
* Publishes a registration name that is used to identify dispatched events and
* can be used with `EventPluginHub.putListener` to register listeners.
*
* @param {string} registrationName Registration name to add.
* @param {object} PluginModule Plugin publishing the event.
* @private
*/
function publishRegistrationName(
registrationName: string,
pluginModule: PluginModule<AnyNativeEvent>,
eventName: string,
): void {
EventPluginRegistry.registrationNameModules[registrationName] = pluginModule;
EventPluginRegistry.registrationNameDependencies[registrationName] =
pluginModule.eventTypes[eventName].dependencies;
}
至此EventPluginRegistry模块完成注册,EventPluginHub可以很容易的调用EventPluginRegistry.getPluginModuleForEvent函数获取事件对应的事件插件,以供生成合成事件对象使用,这个以后会详细说。
二,react事件机制——事件注册
事件注册阶段主要做两件事情,一个是为document绑定上的对应事件回调函数,一个是把react事件回调函数存到EventPluginHub中。
事件注册的入口文件为src/renderers/dom/shared/ReactDOMComponent.js中,react在挂载(mount)dom型元素之前,会遍历元素的props,对不同的类型的props做不同处理。如果propKey是react事件,则执行enqueuePutListener方法进行事件注册。具体代码如下:
// other codes
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
deleteListener(this, propKey);
}
} else if (isCustomComponent(this._tag, nextProps)) {
// other codes
没错,registrationNameModules就是前面准备阶段生成的EventPluginRegistry模块的registrationNameModules,凡是里面“留名”的都是事件,都要进行事件注册。enqueuePutListener代码如下:
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;
var doc = isDocumentFragment
? containerInfo._node
: containerInfo._ownerDocument;
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});
}
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(
listenerToPut.inst,
listenerToPut.registrationName,
listenerToPut.listener,
);
}
上面的listenTo方法就是ReactBrowserEventEmitter.listenTo方法,用于完成事件注册阶段的第一项工作——为document绑定上的对应事件回调函数; putListener调用EventPluginHub.putListener函数完成事件注册阶段的第二项工作——把react事件回调函数存到EventPluginHub中;下面分别来看:
ReactBrowserEventEmitter.listenTo的代码如下:
/**
* We listen for bubbled touch events on the document object.
*
* Firefox v8.01 (and possibly others) exhibited strange behavior when
* mounting `onmousemove` events at some node that was not the document
* element. The symptoms were that if your mouse is not moving over something
* contained within that mount point (for example on the background) the
* top-level listeners for `onmousemove` won't be called. However, if you
* register the `mousemove` on the document object, then it will of course
* catch all `mousemove`s. This along with iOS quirks, justifies restricting
* top-level listeners to the document object only, at least for these
* movement types of events and possibly all events.
*
* @see http://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
*
* Also, `keyup`/`keypress`/`keydown` do not bubble to the window on IE, but
* they bubble to document.
*
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {object} contentDocumentHandle Document which owns the container
*/
listenTo: function(registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
var dependencies =
EventPluginRegistry.registrationNameDependencies[registrationName];
for (var i = 0; i < dependencies.length; i++) {
var dependency = dependencies[i];
if (
!(isListening.hasOwnProperty(dependency) && isListening[dependency])
) {
if (dependency === 'topWheel') {
if (isEventSupported('wheel')) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
'topWheel',
'wheel',
mountAt,
);
} else if (isEventSupported('mousewheel')) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
'topWheel',
'mousewheel',
mountAt,
);
} else {
// Firefox needs to capture a different mouse scroll event.
// @see http://www.quirksmode.org/dom/events/tests/scroll.html
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
'topWheel',
'DOMMouseScroll',
mountAt,
);
}
} else if (dependency === 'topScroll') {
if (isEventSupported('scroll', true)) {
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(
'topScroll',
'scroll',
mountAt,
);
} else {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
'topScroll',
'scroll',
ReactBrowserEventEmitter.ReactEventListener.WINDOW_HANDLE,
);
}
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
if (isEventSupported('focus', true)) {
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(
'topFocus',
'focus',
mountAt,
);
ReactBrowserEventEmitter.ReactEventListener.trapCapturedEvent(
'topBlur',
'blur',
mountAt,
);
} else if (isEventSupported('focusin')) {
// IE has `focusin` and `focusout` events which bubble.
// @see http://www.quirksmode.org/blog/archives/2008/04/delegating_the.html
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
'topFocus',
'focusin',
mountAt,
);
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
'topBlur',
'focusout',
mountAt,
);
}
// to make sure blur and focus event listeners are only attached once
isListening.topBlur = true;
isListening.topFocus = true;
} else if (topEventMapping.hasOwnProperty(dependency)) {
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
dependency,
topEventMapping[dependency],
mountAt,
);
}
isListening[dependency] = true;
}
}
},
listenTo方法首先根据registrationName获取通过EventPluginRegistry.registrationNameDependencies获取对应的top事件名。registrationName就是像onClick这样的绑到react dom元素上的事件名,top事件名是对react可绑定到document上的事件起的标记名如topClick,可根据top事件名找到要绑定到document上的真正事件名如click。再来捋一捋,遇到onClick事件时,先根据onClick找到topClick,再根据topClick找到要向document绑定的真实事件click。具体绑定方法就是执行ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(对,这个ReactEventListener就是前面准备阶段注册的ReactEventListener)这个方法:
/**
* Traps top-level events by using event bubbling.
*
* @param {string} topLevelType Record from `EventConstants`.
* @param {string} handlerBaseName Event name (e.g. "click").
* @param {object} element Element on which to attach listener.
* @return {?object} An object with a remove function which will forcefully
* remove the listener.
* @internal
*/
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element,
handlerBaseName,
ReactEventListener.dispatchEvent.bind(null, topLevelType),
);
},
EventListener.listen兼容了不同浏览器,向document上设置了具体的事件监听回调函数为ReactEventListener.dispatchEvent.bind(null, topLevelType);EventListener.listen代码如下:
/**
* Listen to DOM events during the bubble phase.
*
* @param {DOMEventTarget} target DOM element to register listener on.
* @param {string} eventType Event type, e.g. 'click' or 'mouseover'.
* @param {function} callback Callback function.
* @return {object} Object with a `remove` method.
*/
listen: function listen(target, eventType, callback) {
if (target.addEventListener) {
target.addEventListener(eventType, callback, false);
return {
remove: function remove() {
target.removeEventListener(eventType, callback, false);
}
};
} else if (target.attachEvent) {
target.attachEvent('on' + eventType, callback);
return {
remove: function remove() {
target.detachEvent('on' + eventType, callback);
}
};
}
},
至此,事件注册阶段为document绑定上的对应事件回调函数完成。现在,对于每一个react事件都在document上绑定了对应的事件回调函数ReactEventListener.dispatchEvent.bind(null, topLevelType)。大家也看到了,所有事件都是触发的ReactEventListener.dispatchEvent函数,只是 topLevelType这个参数不同而已。至于dispatchEvent是怎么执行具体的react事件回调函数的,下面慢慢看。
为了让你能联系起来先回忆一下,事件注册阶段的起始是在react挂载dom型元素之前遍历元素的props对不同类型的props做不同处理。对于react事件类型的props,执行ReactBrowserEventEmitter.listenTo方法为document绑定上对应事件回调函数,上面解释的正是这部分。另一部分是调用EventPluginHub.putListener函数完成事件注册阶段的第二项工作——把react事件回调函数存到EventPluginHub中;EventPluginHub.putListener的代码如下:
/**
* Stores `listener` at `listenerBank[registrationName][key]`. Is idempotent.
*
* @param {object} inst The instance, which is the source of events.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @param {function} listener The callback to store.
*/
putListener: function(inst, registrationName, listener) {
var key = getDictionaryKey(inst);
var bankForRegistrationName =
listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[key] = listener;
var PluginModule =
EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
},
这块逻辑比较简单,前面的代码就是把事件回调函数按照事件名->组件实例->回调函数三级存到listenerBank对象中。后面的代码是对于某些事件需要在储存回调函数时执行一些特殊逻辑,暂时可以不用关注。
三,react事件机制——事件触发
事件触发过程当然就是事件发生后代码是怎么执行的了。在前面讲的事件注册阶段,对于每一个react事件,已经在document上绑定了ReactEventListener.dispatchEvent.bind(null, topLevelType)函数,并把事件对应的回调函数存到了EventPluginHub的listenerBank中。所以当事件发生后会触发ReactEventListener.dispatchEvent函数,代码如下(src/renderers/dom/client/ReactEventListener.js):
dispatchEvent: function(topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(
topLevelType,
nativeEvent,
);
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
},
TopLevelCallbackBookKeeping.getPooled生成一个TopLevelCallbackBookKeeping的实例(这里不展开,不明白的同学学习一下react PooledClass类)。ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping)执行后首先把当前正在更新中的标记设为true进入批量更新状态, 然后执行handleTopLevelImpl(bookKeeping),最后批量更新所有状态发生变化的组件。因此不管一次事件执行多少次setState,UI只更新一次。但本文是介绍事件机制的,因此只关注handleTopLevelImpl(bookKeeping)是怎样调用保存起来的事件回调函数的。看一下handleTopLevelImpl的代码:
function handleTopLevelImpl(bookKeeping) {
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
nativeEventTarget,
);
// Loop through the hierarchy, in case there's any nested components.
// It's important that we build the array of ancestors before calling any
// event handlers, because event handlers can modify the DOM, leading to
// inconsistencies with ReactMount's node cache. See #1105.
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),
);
}
}
handleTopLevelImpl首先根据原生事件对象找到dom元素,再通过dom元素找到对应reactDOMComponent的实例;最后获取祖先组件,把他们推入bookKeeping的ancestors数组中。这里的ancestor容易引起误会,ancestor不是当前元素实例的父实例,其获取代码如下:
/**
* Find the deepest React component completely containing the root of the
* passed-in instance (for use when entire React trees are nested within each
* other). If React trees are not nested, returns null.
*/
function findParent(inst) {
// TODO: It may be a good idea to cache this to prevent unnecessary DOM
// traversal, but caching is difficult to do correctly without using a
// mutation observer to listen for all DOM changes.
while (inst._hostParent) {
inst = inst._hostParent;
}
var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
var container = rootNode.parentNode;
return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}
也就是,如果当前react组件树是挂载在另一个react组件树下,就把父组件树的挂载元素对应的reactDOMComponent的实例返回。所以绝大部分情况下,bookKeeping.ancestors中只有一个元素,就是事件触发的dom元素对应的reactDOMComponent实例;
然后对bookKeeping.ancestors每一个reactDOMComponent实例执行ReactEventListener._handleTopLevel,这里的_handleTopLevel在准备阶段里被注入成ReactEventEmitterMixin.handleTopLevel函数。代码如下(src/renderers/shared/stack/reconciler/ReactEventEmitterMixin.js):
/**
* Streams a fired top-level event to `EventPluginHub` where plugins have the
* opportunity to create `ReactEvent`s to be dispatched.
*/
handleTopLevel: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
var events = EventPluginHub.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
runEventQueueInBatch(events);
},
handleTopLevel首先执行EventPluginHub.extractEvents生成合成事件对象,然后执行runEventQueueInBatch并执行所有react事件回调函数。
先捋一遍生成合成事件对象的代码:
/**
* Allows registered plugins an opportunity to extract events from top-level
* native browser events.
*
* @return {*} An accumulation of synthetic events.
* @internal
*/
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;
},
从EventPluginRegistry.plugins中依次取出事件插件,并用取出的事件插件的extractEvents方法构建合成事件对象,如果构建成功将合成事件对象加入events数组中。下面以SimpleEventPlugin为例说明事件插件的extractEvents方法说明(src/renderers/dom/client/eventPlugins/SimpleEventPlugin.js):
extractEvents: function(
topLevelType: TopLevelTypes,
targetInst: ReactInstance,
nativeEvent: MouseEvent,
nativeEventTarget: EventTarget,
): null | ReactSyntheticEvent {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
}
var EventConstructor;
switch (topLevelType) {
case 'topAbort':
case 'topCanPlay':
case 'topCanPlayThrough':
case 'topDurationChange':
case 'topEmptied':
case 'topEncrypted':
case 'topEnded':
case 'topError':
case 'topInput':
case 'topInvalid':
case 'topLoad':
case 'topLoadedData':
case 'topLoadedMetadata':
case 'topLoadStart':
case 'topPause':
case 'topPlay':
case 'topPlaying':
case 'topProgress':
case 'topRateChange':
case 'topReset':
case 'topSeeked':
case 'topSeeking':
case 'topStalled':
case 'topSubmit':
case 'topSuspend':
case 'topTimeUpdate':
case 'topVolumeChange':
case 'topWaiting':
// HTML Events
// @see http://www.w3.org/TR/html5/index.html#events-0
EventConstructor = SyntheticEvent;
break;
case 'topKeyPress':
// Firefox creates a keypress event for function keys too. This removes
// the unwanted keypress events. Enter is however both printable and
// non-printable. One would expect Tab to be as well (but it isn't).
if (getEventCharCode(nativeEvent) === 0) {
return null;
}
/* falls through */
case 'topKeyDown':
case 'topKeyUp':
EventConstructor = SyntheticKeyboardEvent;
break;
case 'topBlur':
case 'topFocus':
EventConstructor = SyntheticFocusEvent;
break;
case 'topClick':
// Firefox creates a click event on right mouse clicks. This removes the
// unwanted click events.
if (nativeEvent.button === 2) {
return null;
}
/* falls through */
case 'topDoubleClick':
case 'topMouseDown':
case 'topMouseMove':
case 'topMouseUp':
// TODO: Disabled elements should not respond to mouse events
/* falls through */
case 'topMouseOut':
case 'topMouseOver':
case 'topContextMenu':
EventConstructor = SyntheticMouseEvent;
break;
case 'topDrag':
case 'topDragEnd':
case 'topDragEnter':
case 'topDragExit':
case 'topDragLeave':
case 'topDragOver':
case 'topDragStart':
case 'topDrop':
EventConstructor = SyntheticDragEvent;
break;
case 'topTouchCancel':
case 'topTouchEnd':
case 'topTouchMove':
case 'topTouchStart':
EventConstructor = SyntheticTouchEvent;
break;
case 'topAnimationEnd':
case 'topAnimationIteration':
case 'topAnimationStart':
EventConstructor = SyntheticAnimationEvent;
break;
case 'topTransitionEnd':
EventConstructor = SyntheticTransitionEvent;
break;
case 'topScroll':
EventConstructor = SyntheticUIEvent;
break;
case 'topWheel':
EventConstructor = SyntheticWheelEvent;
break;
case 'topCopy':
case 'topCut':
case 'topPaste':
EventConstructor = SyntheticClipboardEvent;
break;
}
invariant(
EventConstructor,
'SimpleEventPlugin: Unhandled event type, `%s`.',
topLevelType,
);
var event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
},
主要是根据topLevelType选择对应的合成事件类型,生成合成事件对象。然后执行EventPropagators.accumulateTwoPhaseDispatches(event)找到所有监听过此事件的reactDom实例放入事件对象放入合成事件对象的_dispatchInstances中,把对应实例绑定的事件回调函数放入合成事件对象的_dispatchListeners中。其代码如下(src/renderers/shared/stack/event/EventPropagators.js):
function accumulateTwoPhaseDispatches(events) {
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle)其实就是对events中每一个合成事件对象event执行accumulateTwoPhaseDispatchesSingle(event)。accumulateTwoPhaseDispatchesSingle代码如下:
/**
* Collect dispatches (must be entirely collected before dispatching - see unit
* tests). Lazily allocate the array to conserve memory. We must loop through
* each event and perform the traversal for each one. We cannot perform a
* single traversal for the entire collection of events because each event may
* have a different target.
*/
function accumulateTwoPhaseDispatchesSingle(event) {
if (event && event.dispatchConfig.phasedRegistrationNames) {
EventPluginUtils.traverseTwoPhase(
event._targetInst,
accumulateDirectionalDispatches,
event,
);
}
}
EventPluginUtils.traverseTwoPhase方法其实执行的是准备阶段注册进EventPluginUtils里的ReactDOMTreeTraversal的traverseTwoPhase方法,代码如下(src/renderers/dom/client/ReactDOMTreeTraversal.js):
/**
* Simulates the traversal of a two-phase, capture/bubble event dispatch.
*/
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = inst._hostParent;
}
var i;
for (i = path.length; i-- > 0; ) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
traverseTwoPhase先把当前实例的所有父元素实例推进path数组中,然后先从顶层实例开始遍历path数组执行accumulateDirectionalDispatches函数(模拟事件捕获阶段);再从下层实例开始path数组执行accumulateDirectionalDispatches函数(模拟事件冒泡阶段);accumulateDirectionalDispatches函数代码如下:
/**
* Tags a `SyntheticEvent` with dispatched listeners. Creating this function
* here, allows us to not have to bind or create functions for each event.
* Mutating the event's members allows us to not have to create a wrapping
* "dispatch" object that pairs the event with the listener.
*/
function accumulateDirectionalDispatches(inst, phase, event) {
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
/**
* Some event types have a notion of different registration names for different
* "phases" of propagation. This finds listeners by a given phase.
*/
function listenerAtPhase(inst, event, propagationPhase: PropagationPhases) {
var registrationName =
event.dispatchConfig.phasedRegistrationNames[propagationPhase];
return getListener(inst, registrationName);
}
还记得面前讲的怎样把事件回调函数保存在EventPluginHub中的吗?accumulateDirectionalDispatches首先通过getListener方法通过事件名称和react dom实例从EventPluginHub中取出绑定对应元素绑定的事件回调函数。getListener就是EventPluginHub的getListener方法:
/**
* @param {object} inst The instance, which is the source of events.
* @param {string} registrationName Name of listener (e.g. `onClick`).
* @return {?function} The stored callback.
*/
getListener: function(inst, registrationName) {
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
var bankForRegistrationName = listenerBank[registrationName];
if (
shouldPreventMouseEvent(
registrationName,
inst._currentElement.type,
inst._currentElement.props,
)
) {
return null;
}
var key = getDictionaryKey(inst);
return bankForRegistrationName && bankForRegistrationName[key];
},
如果有事件回调函数的话,把相应react dom实例放入放入合成事件对象的_dispatchInstances中,把对应实例绑定的事件回调函数放入合成事件对象的_dispatchListeners中。
这样经过执行accumulateTwoPhaseDispatchesSingle(event),每一个合成事件对象中都有_dispatchListeners数组保存从捕获阶段到冒泡阶段该事件的所有回调函数,都有_dispatchInstances数组保存_dispatchListeners中回调函数对应的react dom组件实例。
至此,合成事件对象构建完毕,如果感觉混乱的话再从EventPluginHub.extractEvents函数多捋几遍。
在讲合成事件对象构建之前,我们讲到ReactEventEmitterMixin.handleTopLevel函数,先执行EventPluginHub.extractEvents构建合成事件对象,再通过runEventQueueInBatch(events)执行所有回调函数,现在看看runEventQueueInBatch的代码:
function runEventQueueInBatch(events) {
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);
}
EventPluginHub.enqueueEvents把所有合成事件对象加入到EventPluginHub的eventQueue中
/**
* Enqueues a synthetic event that should be dispatched when
* `processEventQueue` is invoked.
*
* @param {*} events An accumulation of synthetic events.
* @internal
*/
enqueueEvents: function(events) {
if (events) {
eventQueue = accumulateInto(eventQueue, events);
}
},
EventPluginHub.processEventQueue对每一个合成事件对象event执行executeDispatchesAndReleaseTopLevel(event):
/**
* Dispatches all synthetic events on the event queue.
*
* @internal
*/
processEventQueue: function(simulated) {
// Set `eventQueue` to null before processing it so that we can tell if more
// events get enqueued while processing.
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseSimulated,
);
} else {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
// This would be a good time to rethrow if any of the event handlers threw.
ReactErrorUtils.rethrowCaughtError();
},
executeDispatchesAndReleaseTopLevel具体调用EventPluginUtils中的方法按顺序对合成事件对象中的回调函数执行:
var executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e, false);
};
/**
* Dispatches an event and releases it back into the pool, unless persistent.
*
* @param {?object} event Synthetic event to be dispatched.
* @param {boolean} simulated If the event is simulated (changes exn behavior)
* @private
*/
var executeDispatchesAndRelease = function(event, simulated) {
if (event) {
EventPluginUtils.executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
EventPluginUtils.executeDispatchesInOrder代码如下:
/**
* Standard/simple iteration through an event's collected dispatches.
*/
function executeDispatchesInOrder(event, simulated) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
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;
}
/**
* Dispatch the event to the listener.
* @param {SyntheticEvent} event SyntheticEvent to handle
* @param {boolean} simulated If the event is simulated (changes exn behavior)
* @param {function} listener Application-level callback
* @param {*} inst Internal component instance
*/
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;
}
对合成事件对象存在_dispatchListeners中的每一个事件回调函数通过executeDispatch函数执行,executeDispatch主要是把合成事件对象的currentTarget属性设为回调函数执行时对应元素的node节点,并用ReactErrorUtils.invokeGuardedCallback方法执行回调函数方便捕获错误。
至此,所有回调函数执行完毕事件触发阶段完毕。总结一下事件触发过程:
- 1、dispatchEvent生成bookKeeping, 调用handleTopLevelImpl 。
- 2、handleTopLevelImpl找到所有祖先组件实例,调用ReactEventListener._handleTopLevel既ReactEventEmitterMixin.handleTopLevel。
- 3、handleTopLevel首先执行EventPluginHub.extractEvents生成合成事件对象,然后执行runEventQueueInBatch并执行所有react事件回调函数,
- 4、生成合成对象的过程是:首先调用事件对应的事件插件生成合成事件对象,然后遍历事件触发元素的父组件实例,找到所有事件回调函数储存在合成事件对象的_dispatchListeners数组。
- 5、执行回调函数的过程是:先把所有合成事件对象加入到EventPluginHub的eventQueue中,然后对依次执行合成事件对象中储存的回调函数。
到了这里react事件机制所有代码都讲完了,个人感觉react事件机制的代码是相当复杂,学习时建议按我分的三个阶段把每一个阶段的代码弄清楚了再看下一个阶段的代码。一定要有耐心,要一点一点抠。
由于作者本人水平有限,文章中肯定有很多错误,欢迎批评指正。