React Event的主要四个文件是 ReactBrowerEventEmitter.js(负责节点绑定的回调函数,该回调函数执行过程中构建合成事件对象,获取组件实例的绑定回调并执行,若有state变更,则重绘组件),ReactEventListener.js(负责事件注册和事件分发),ReactEventEmitter(负责事件的执行),EventPluginHub.js(负责事件的存储)和ReactEventEmitterMixin.js。
React的事件机制有2个特点:
a. 使用事件委托机制,以队列的方式,从触发事件的组件向父组件回溯直到document节点,因此React组件上声明的事件最终绑定到了document 上。由此减少了DOM操作,优化性能。
b. 基于虚拟DOM实现SyntheticEvent合成事件
1.事件注册
在上一篇文章的提到onClick事件的例子,如下:
var LikeButton = React.createClass({
getInitialState: function() {
return {value: 0};
},
handleClick: function(event) {
this.setState({value: this.state.value + 1});
this.setState({value: this.state.value + 2});
},
render: function() {
var text = '当前值为'+this.state.value;
return (
{text}
);
}
});
那么onClick事件是如何被注册到事件机制内部的呢?试想一下,在《React渲染机制》中我们已经知道,ReactDOM.render()会调用_renderSubtreeIntoContainer方法将新的Element添加到container中。_renderSubtreeIntoContainer方法首先会创建新Element 的虚拟DOM,在该虚拟DOM上进行操作。在将虚拟DOM转换组件的回调函数应该是在其创建(或更新的入口)作为该组件的属性存在的,在ReactDOMComponent.Mixin中,DOMComponent共享的创建和更新组件的入口方法mountComponent和updateComponent中,使用_updateDOMProperties方法来更新虚拟DOM属性。
_updateDOMProperties: function (lastProps, nextProps, transaction) {
var propKey;
var styleName;
var styleUpdates;
for (propKey in lastProps) {
if (registrationNameModules.hasOwnProperty(propKey)) {
if (lastProps[propKey]) {
deleteListener(this, propKey);
}
}
}
for (propKey in nextProps) {
if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
deleteListener(this, propKey);
}
}
}
通过enqueuePutListener方法负责注册和存储事件
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return;
}
if (process.env.NODE_ENV !== 'production') {
// document 节点
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
//将事件注册到DOM节点
listenTo(registrationName, doc);
// 事件存储
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
(1)listenTo() 负责注册事件,
可以发现React的事件机制框架
(2)采用事务队列的方式调用putListener将注册的事件存储起来,以供事件触发时回调。
在此先对listenTo()注册事件进行分析,后面再分析负责事件存储的putListener。
由listenTo = ReactBrowserEventEmitter.listenTo;在ReactBrowserEventEmitter
.js文件下找到listenTo方法,可以发现它主要解决了不同浏览器间捕获和冒泡不兼容的问题。事件回调方法在bubble阶段被触发。如果我们想让它在capture阶段触发,则需要在事件名上加capture。调用trapCapturedEvent和trapBubbledEvent来注册捕获和冒泡事件。
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;
}
}
}
2. 事件存储
事件存储由EventPluginHub来负责,它的入口在我们上面讲到的enqueuePutListener中的putListener方法,如下
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
实际调用的是EventPluginHub.js中的putListener方法,EventPluginHub.js主要负责事件的存储、合成事件以对象池的方式实现创建和销毁。
putListener: function (inst, registrationName, listener) {
// key用来标识被注册事件react对象的NoodID。
var key = getDictionaryKey(inst);
//如果listenerBank存在registrationName事件元素取出该值,否则初始化。比如该实例中listenerBank[‘OnClick’]
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
//将该组件的注册的OnClick事件存入,即listenerBank[‘OnClick’][key] = listener; bankForRegistrationName[key] = listener;
// EventPluginRegistry事件注册 插件,用于合成和分发事件,EventPluginRegistry.registrationNameModules[registrationName]用来提取’OnClick’的事件回调插件模型。
var PluginModule =
EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
}
3.事件分发
当事件触发时,注册在document上的回调函数会被触发。事件触发的入口函数是ReactEventListener.dispatchEvent负责分发已经注册的回调函数。在这个函数中会调用batchingStrategy 的 batchUpdate 方法实现批量处理更新。batchUpdate以transaction形式调用,批量处理更新。
@param topLevelType 标识事件名
@param nativeEvent 用户在浏览器上触发的原生事件
dispatchEvent: function (topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}
// 从对象池中提取事件对象
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
try {
// 将React事件流放入批量处理队列中
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
// 回调执行结束后释放对象
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
ReactUpdates.batchedUpdates()的第一个参数是批量处理中的回调函数,后面的参数是传入回调函数的参数。
TopLevelCallbackBookKeeping是一个类,该类对象用于记录topLevelType,nativeEvent和用于存储所有的祖先节点数组ancestors(当前是空的,只有分发时才会遍历并存储所有的祖先节点) 。
那么传入batchedUpdates 内部的回调函数handleTopLevelImpl是什么呢???它其实就是事件分发的核心部分。
function handleTopLevelImpl(bookKeeping) {
// 获取原生的事件target
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 获取原生事件的target说在的组件,它是虚拟DOM
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
var ancestor = targetInst;
// 向上遍历所有的祖先节点并存到bookKeeping.ancestors 中。
//因为事件回调中可能会改变Virtual DOM结构,所以要先遍历好组件层级
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的实现方法在ReactBrowserEventEmitter.js里。
4.事件回调
从上一节中知道各个节点分别调用ReactEventListener._handleTopLevel()方法来触发被注册的回调函数。
_handleTopLevel 通过ReactDefaultInjection模块注入ReactEventListener模块,设为ReactBrowserEventEmitter.ReactEventListener属性 。ReactBrowserEventEmitter为ReactEventListener属性提供_handleTopLevel方法,即
ReactEventListener.setHandleTopLevel(ReactBrowserEventEmitter.handleTopLevel);
handleTopLevel实际来自于ReactEventEmitterMixin模块的handleTopLevel:
handleTopLevel: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
//采用对象池的方式构造出合成事件。不同的eventType的合成事件可能不同
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 批处理队列中的events
runEventQueueInBatch(events);
}
4.1合成事件
合成事件时一个跨浏览器原生事件包装器,具有与浏览器原生事件相同的接口,包括 stopPropagation() 和 preventDefault() ,除了事件在所有浏览器中他们工作方式都相同。现在来看一看React是如何实现合成事件的。
EventPluginHub.js中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;
}
EventPluginHub.extractEvents()方法是通过执行各个插件的extractEvents方法来创建合成事件。possiblePlugin.extractEvents()根据事件的Type创建不同插件的合成事件,accumulateInto()负责将所有插件的合成事件存入到events数组中,形成当前事件的合成事件集合。EventPluginRegistry.plugins默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,分别是 SimpleEventPlugin、EnterLeaveEventPlugin、ChangeEventPlugin、SelectEventPlugin和BeforeInputEventPlugin。根据不同的事件类型采用不同的事件合成方法,这些事件合成方法有SyntheticAnimationEvent.js、SyntheticFocusEvent.js、SyntheticKeyboardEvent.js、SyntheticMouseEvent.js、SyntheticTouchEvent.js、SyntheticUIEvent.js、SyntheticWheelEvent.js等等一共13种合成方法。以SyntheticMouseEvent为例:
var MouseEventInterface = {
screenX: null,
screenY: null,
clientX: null,
clientY: null,
ctrlKey: null,
shiftKey: null,
altKey: null,
metaKey: null,
getModifierState: getEventModifierState,
button: function (event) {
// Webkit, Firefox, IE9+
// which: 1 2 3
// button: 0 1 2 (standard)
var button = event.button;
if ('which' in event) {
return button;
}
// IE<9
// which: undefined
// button: 0 0 0
// button: 1 4 2 (onmouseup)
return button === 2 ? 2 : button === 4 ? 1 : 0;
},
buttons: null,
relatedTarget: function (event) {
return event.relatedTarget || (event.fromElement === event.srcElement ? event.toElement : event.fromElement);
},
// "Proprietary" Interface.
pageX: function (event) {
return 'pageX' in event ? event.pageX : event.clientX + ViewportMetrics.currentScrollLeft;
},
pageY: function (event) {
return 'pageY' in event ? event.pageY : event.clientY + ViewportMetrics.currentScrollTop;
}
};
function SyntheticMouseEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
return SyntheticUIEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
}
module.exports = SyntheticMouseEvent;
该方法主要考虑了mouseEvent在各个浏览器之间的兼容性问题。其返回值是SyntheticUIEvent执行的结果。SyntheticUIEvent.js中找到SyntheticUIEvent方法
function SyntheticUIEvent(dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget) {
return SyntheticEvent.call(this, dispatchConfig, dispatchMarker, nativeEvent, nativeEventTarget);
}
SyntheticUIEvent是SyntheticEvent调用执行的结果,SyntheticEvent.js中主要是对合成事件的接口进行了封装,包括preventDefault()、stopPropagation()、persist()、isPersistent()、destructor()。
注意: 合成事件对象都是以pool方式创建和销毁的,这提高了React的性能,同时也意味着一旦事件执行结束该合成事件对象会被销毁。因此不能通过异步方式获取该事件。如下:
function onClick(event) {
console.log(event); // => nullified object.
console.log(event.type); // => "click"
const eventType = event.type; // => "click"
setTimeout(function() {
console.log(event.type); // => null
console.log(eventType); // => "click"
}, 0);
// Won't work. this.state.clickEvent will only contain null values.
this.setState({clickEvent: event});
// You can still export event properties.
this.setState({eventType: event.type});
}
如果想继续使用该事件可以使用e.persist()方法。
4.2事件执行
事件合成之后就需要执行事件,React以批量处理事件队列的方式执行事件的。它的入口函数是在ReactEventEmitterMixin.js中的runEventQueueInBatch方法:
function runEventQueueInBatch(events) {
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);
}
EventPluginHub.enqueueEvents()方法是将合成事件写入队列,EventPluginHub.processEventQueue()方法是执行队列中的事件。
EventPluginHub.enqueueEvents()方法的实现逻辑很简单,使用accumulateInto方法将events存入eventQueue队列中。
enqueueEvents: function (events) {
if (events) {
eventQueue = accumulateInto(eventQueue, events);
}
}
EventPluginHub.processEventQueue()方法的实现逻辑分为:
processEventQueue: function (simulated) {
// 现将eventQueue 设置为null,以便在处理过程中让新合成事件入队列
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();
}
(1)获取合成事件队列,其中可能包含之前没处理完的合成事件
(2)遍历合成事件队列,
function forEachAccumulated(arr, cb, scope) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
事件队列的回调函数为executeDispatchesAndReleaseSimulated,负责事件的分发和事件遍历结束后是否释放合成事件对象。
var executeDispatchesAndReleaseSimulated = function (e) {
return executeDispatchesAndRelease(e, true);
};
/**/
var executeDispatchesAndRelease = function (event, simulated) {
if (event) {
/*按存储顺序分发事件,先进先出*/
EventPluginUtils.executeDispatchesInOrder(event, simulated);
/*判断是否释放事件*/
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
React的事件系统主要分为3个步骤:
一是事件绑定,ReactBrowserEventEmitter的trapBubbledEvent等方法为节点或文档绑定事件;
二是事件监听,ReactEventListener.dispatchEvent将把该回调函数分发给事件对象的_dispatchListener属性;调用ReactBrowserEventEmitter.ReactEventListener方法以监听节点事件。
三是事件分发与触发,对触发的事件进行分发,并创建合成事件对象,在回调中用构建合成事件对象并执行合成事件对的象绑定回调。