本文使用的是react 15.6.1的代码
我们先看一段代码
import React, {Component} from "react";
import "./App.css";
class App extends Component {
componentDidMount() {
this.refs.div.addEventListener('click', function () {
alert('dom event');
})
}
componentClick(event) {
alert('react event');
event.stopPropagation();
}
render() {
return (
<div ref="div">
<button ref="button" onClick={this.componentClick.bind(this)}>
button
</button>
</div>
);
}
}
export default App;
当我们点击按钮的时候,会弹出哪一些框?
答案是分别弹出 dom event以及react event,如果只了解dom事件的童鞋也许会问,在componentClick中明明写了stopPropagation,事件应该不会冒泡到父元素了啊?对此有疑惑的童鞋可以看看下面的分析,帮助你更进一步了解react的事件系统。
事件的注册
熟悉react的同学应该知道,在react中,几乎所有的事件都被代理到了document之上,以达到优化的目的,试想,如果react组件又1000个列表,每个列表都给他绑定一个click事件是多么费性能的事情,即便不用react,我们也会使用代理来处理这样绑定事件过多对性能的开销。既然事件大部分都绑定到了document上,所以stopPropagation没有起到相应的作用,我们来看看是如何绑定的;
在ReactDomComponent.js中有这样一段代码,react组件渲染props的时候,发现如果是事件属性,则会调用这样一个函数enqueuePutListener
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (transaction instanceof ReactServerRenderingTransaction) {
return;
}
//实体对象中dom节点信息
var containerInfo = inst._hostContainerInfo;
var isDocumentFragment =
containerInfo._node && containerInfo._node.nodeType === DOC_FRAGMENT_TYPE;
/**
* ownerDocument:该节点的顶层document
* [ownerDocument](https://developer.mozilla.org/zh-CN/docs/Web/API/Node/ownerDocument)
*/
var doc = isDocumentFragment
? containerInfo._node
: containerInfo._ownerDocument;
//注册事件
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});
}
在这段代码中,简单做了这样一件事情,首先获取到真实节点,判断节点是否是document,如果不是,则将其节点的顶层document以及事件名传入listenTo方法,对应的listenTo在ReactBrowserEventEmitter.js中,如下
listenTo: function(registrationName, contentDocumentHandle) {
// 事件挂载节点,大部分是document
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') {
//.....
} else if (dependency === 'topScroll') {
//.....
} else if (dependency === 'topFocus' || dependency === 'topBlur') {
//.....
} else if (topEventMapping.hasOwnProperty(dependency)) {
//注册冒泡事件
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
dependency,
topEventMapping[dependency],
mountAt,
);
}
isListening[dependency] = true;
}
}
},
在这个方法中,着重对不同浏览器捕获与冒泡不同进行兼容处理,同时,对wheel scroll等事件进行了特殊处理,调用trapCapturedEvent和trapBubbledEvent来注册捕获和冒泡事件,我们来看看trapBubbledEvent的实现
return ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(
topLevelType,
handlerBaseName,
handle,
);
代码很简单,调用了ReactEventListener的trapBubbledEvent方法,可是ReactEventListener对象是从哪里来的呢?在【react源码阅读笔记(3)batchedUpdates与Transaction】介绍过React在启动后会调用inject方法注入一些对象,在ReactDefaultInjection.js中执行了
ReactInjection.EventEmitter.injectReactEventListener(ReactEventListener);
因此,在默认情况下 ReactBrowserEventEmitter.ReactEventListener即为注入的ReactEventListener对象
//ReactEventListener.trapBubbledEvent
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element,
handlerBaseName,
//事件回调,注意
ReactEventListener.dispatchEvent.bind(null, topLevelType),
);
}
这个方法只是简单判断了一下元素是否存在,随后调用EventListener.listen注册事件,EventListener.js位于node_modules/fbjs/lib中
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);
}
};
}
}
代码更加简单了,就是对不同浏览器的addEventListener做兼容处理,到这里我们终于找到了事件注册的源头
但是我们在ReactEventListener.trapBubbledEvent发现了,绑定到dom上的事件回调,并不是我们在react写的对应的回调函数,而是ReactEventListener.dispatchEvent.bind(null, topLevelType),这个又是什么呢?
// topLevelType:带top的事件名,如topClick。不用纠结为什么带一个top字段,知道它是事件名就OK了
// nativeEvent: 用户触发click等事件时,浏览器传递的原生事件
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);
}
}
简单总结就是
- 对dom组件进行遍历,如果是事件的话调用 enqueuePutListener
- 对该dom找到其document,绑定对应的事件名
- document不管注册的是什么事件,具有统一的回调函数handleTopLevelImpl
事件存储
那么,真正的事件回调在哪里呢?
回到 enqueuePutListener方法,在listenTo后面执行了方法enqueue,把真正的事件绑定回调listener最为参数传入putListener函数中,也就是说,在ReactReconcileTransaction事务的close阶段执行notifyAll的时候就会调用putListener方法,
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener,
});
function putListener() {
var listenerToPut = this;
EventPluginHub.putListener(
listenerToPut.inst,
listenerToPut.registrationName,
listenerToPut.listener,
);
}
在这里,调用了 EventPluginHub.putListener方法,将对应的组件对象,事件名以及回调作为参数传递
putListener: function(inst, registrationName, listener) {
// return '.' + inst._rootNodeID;
var key = getDictionaryKey(inst);
// 将所绑定的事件存入 listenerBank中,
var bankForRegistrationName =
listenerBank[registrationName] || (listenerBank[registrationName] = {});
// 将对应的事件回调放入 bankForRegistrationName中,通过id区分不同组件当时相同的事件,如组件A和B都有click事件,通过id进行区分
bankForRegistrationName[key] = listener;
var PluginModule =
EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
}
在这里,react将不同时间存入listenerBank数组中,然后通过NodeId区分不同节点具体所绑定的事件
事件执行
上面说到,react中会将真正的事件回调函数通过不同id,塞入listenerBank中,大部分事件都是绑定到document上,并使用dispatchEvent进行分发事件,最后调用了handleTopLevelImpl,那么来看看handleTopLevelImpl事件做了些什么
function handleTopLevelImpl(bookKeeping) {
// 获取这个event的真是dom元素,如果是text节点,则返回父容器
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 获取事件节点所对应的react实例
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(
nativeEventTarget,
);
var ancestor = targetInst;
// 将该组件所有父节点放入ancestors,因为事件可能会改变父节点结构,因此在执行事件回调之前缓存当前parent
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主要是模拟了事件的冒泡操作,拿到事件的注册对象,然后依次找到其父级组件,调用ReactEventListener._handleTopLeve
ReactEventListener._handleTopLeve本质上是调用ReactBrowserEventEmitter.handleTopLevel的方法,该代码位于ReactEventEmitterMixin中
handleTopLevel: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
//合成对应的事件
var events = EventPluginHub.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
//将事件放入队列中
runEventQueueInBatch(events);
},
在这里,我们发现,这里会将组建实例,具体事件对象等参数调用extractEvents合成react的事件对象,来看看react将事件是如何合成的
事件合成
EventPluginHub.extractEvents方法
extractEvents: function(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
) {
var events;
// 获取事件插件,
var plugins = EventPluginRegistry.plugins;
for (var i = 0; i < plugins.length; i++) {
// 通过各个插件返回事件数组,最后将事件数组合并返回
var possiblePlugin = plugins[i];
if (possiblePlugin) {
var extractedEvents = possiblePlugin.extractEvents(
topLevelType,
targetInst,
nativeEvent,
nativeEventTarget,
);
if (extractedEvents) {
//该方法比较简单,就是,简单来说就是数组合并,
// 如果param1和2都是数组,那么调用concat合并
// 如果其中有一个是对象,将其按顺序插入新的数组之中,
// 最后返回的一定是数组
events = accumulateInto(events, extractedEvents);
}
}
}
return events;
}
这段代码依然相对简单,合成事件的时候,会依次通过EventPluginRegistry.plugins插件列表来生成对应的事件数组,最后将这个生成的事件合并为一个数组返回,
我们来看看具体对应的插件是什么
在我们的老朋友ReactDefaultInjection中,会自动注册5个插件
ReactInjection.EventPluginHub.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin,
});
我们来分析其中一个SimpleEventPlugin插件的extractEvents方法
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 'topCopy':
case 'topCut':
case 'topPaste':
EventConstructor = SyntheticClipboardEvent;
break;
}
// 从缓存池中获取对应的event
var event = EventConstructor.getPooled(
dispatchConfig,
targetInst,
nativeEvent,
nativeEventTarget,
);
//最后实际调用了ReactDOMTraversal中的traverseTwoPhase方法
mao.accumulateTwoPhaseDispatches(event);
return event;
},
function traverseTwoPhase(inst, fn, arg) {
//这里的fn是EventPropagators中的accumulateDirectionalDispatches方法
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);
}
}
在这里对模拟进行了冒泡与捕获,同时调用了accumulateDirectionalDispatches方法,来看看fn中处理了什么
function accumulateDispatches(inst, ignoredDirection, event) {
if (event && event.dispatchConfig.registrationName) {
var registrationName = event.dispatchConfig.registrationName;
var listener = getListener(inst, registrationName);
if (listener) {
// 将具体的event以及实例放入event下面的数组之中
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
}
参考上文,也就是说,合成的event保存了绑定事件节点到其父节点/实例的所有listener以及inst,但是我们还没有看到具体的作用,我们继续分析。
事件分发
事件已经由插件合成完毕,我们回到 handleTopLevel方法,看看 runEventQueueInBatch(events)具体做了些什么?
function runEventQueueInBatch(events) {
// 先将events事件放入队列中,不做分析,类似push
EventPluginHub.enqueueEvents(events);
//触发该事件队列中的所有事件
EventPluginHub.processEventQueue(false);
}
来看看processEventQueue方法
processEventQueue: function(simulated) {
// 获取已合成事件队列
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseSimulated,
);
} else {
// 让所有事件执行executeDispatchesAndReleaseTopLevel方法,
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
// This would be a good time to rethrow if any of the event handlers threw.
ReactErrorUtils.rethrowCaughtError();
}
往下分析
/**
*
* @param e 事件队列中的某个
*/
var executeDispatchesAndReleaseTopLevel = function(e) {
return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function(event, simulated) {
if (event) {
EventPluginUtils.executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
executeDispatchesAndReleaseTopLevel最后本质是调用了EventPluginUtils.executeDispatchesInOrder(event, simulated)方法
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;
}
这段代码相对简单了,如果有处理这个事件的回调函数,就调用executeDispatch进行处理
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
// 获取具体dom
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
// 在15.6版本invokeGuardedCallbackWithCatch和下面的invokeGuardedCallback代码是相同的,具体的代码就是执行listener,并将event作为参数
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
//
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
到这里,拿到真实的dom,将合成的event赋予currentTarget,是不是和原生了event有点神似,最后,执行了所绑定的回调函数,直到这里我们发现了最开始的问题,为什么我们在componentClick中调用的stopPropagation并没有干扰到dom的事件,因为这个时候我们拿到的event并不是真实dom的事件,是完全由react封装实现了标准dom的虚拟事件。
总结
react为什么要做这么一套复杂的事件系统?用原生的不好吗?
我觉得这么设计至少可以有(还有什么好处欢迎告诉我)
- 自己实现事件,可以有效的规避不同浏览器对事件的兼容性错误。
- 全局事件代理,对系统优化无疑的巨大的性能提升。
参考资料
[React源码分析7 — React合成事件系统](http://blog.csdn.net/u013510838/article/details/612247600