react的virtual dom在内存中是以对象的形式存在的。react基于virtual dom实现了一个SyntheticEvent(合成事件)层,在定义的事件处理程序中,接收到的事件对象是一个SyntheticEvent对象的实例,该对象是对nativeEvent做了一层封装,完全符合w3c标准,并解决了各浏览器兼容性问题。
合成事件的绑定方式(jsx语法)
<button onClick={this.handleClick}> test </button>
原生DOM0级事件的绑定方式
<button onclick="handleClick()"> test </button>
react合成事件相对原生事件的特性
1. react底层,是采用事件委托的方式,它并没有把事件处理函数直接绑定到真实节点上,其实是绑定到html文档对象的根元素上(document.documentElement),这种方式对性能有很大的改善。
并且react对Synthetic对象进行利用,并不是每个事件处理程序都传入一个新的合成对象,而是同类型的事件复用一个Synthetic对象,只是修改里面相应的属性,这也节省了内存。
2. react合成事件相当于完全构造了一个新的事件对象,通过事件对象获取的属性,其实是获取Synthetic对象上的属性,而不是原生对象上的,通过重新事件对象解决了兼容性问题。
3.
react合成事件类型:(https://facebook.github.io/react/docs/events.html)
....
源码讲解:
1. EventConstants.js ,定义了一些常量,将原生所有的事件类型对应到react的事件系统中,‘click’ => 'topClick',但是写法还是onClick
2. react事件系统,通过多种插件系统组成
SimpleEventPlugin.js,通过判断不同的基本事件类型,创建不同的合成对象类型(SyntheticFocusEvent, SyntheticMouseEvent....)
react事件流程
3.ReactDomComponent.js
_createOpenTagMarkupAndPutListeners 函数,解析react element上的属性
enqueuePutListener(this, 'onClick', function() {} , transaction) 函数,
提取事件类型的名字和事件处理程序进行注册
listenTo('onClick', documentElement),在html元素上绑定click事件
但是每次事件首先出发的是EventListener.dispatchEvent函数
原生事件系统,事件分为两个主要步骤:事件注册(在元素上绑定某个类型的事件),事件触发(通过交互动作,执行事件处理程序)
react自己内部实现了一个高效的事件注册、存储、分发执行、重用合成对象的事件系统,在原来的Dom事件体系上做了很大改进,减少了内存消耗,简化了事件逻辑,并最大化的解决了IE等浏览器的不兼容问题。与DOM事件体系相比,它有如下特点:
不同类型事件多个事件处理程序
而React组件上声明的事件,最终是绑定到了html文档的根元素上(document.documentElement),而不是React组件对应的dom节点。故只有document这个节点上面才绑定了DOM原生事件,其他节点没有绑定事件。这样简化了DOM原生事件,减少了内存开销。
并且在document节点上,对同类型的事件注册,只会注册一个(比如:在多个组件上注册了onClick事件,也只会在document节点上注册一个onClick事件)
2. React有一套自己的合成事件SyntheticEvent,不同类型的事件会构造不同的SyntheticEvent
SyntheticMouseEvent
SyntheticSelectionEvent
SyntheticClipboardEvent
SyntheticFocusEvent
......
3. React自身实现了一套事件捕获冒泡机制。
在事件触发的时候,提取合成事件,从触发事件的组件开始,向父级..上级遍历,此路径上的实例,反向遍历获得捕获阶段的listener,存放到_dispatchListeners上,此路径上的实例正向遍历获得冒泡阶段的listener存放到_dispatchListeners上。事件执行时,只要顺序执行_dispatchListeners里的函数。这样也就模拟实现了捕获冒泡机制。
3. React使用对象池来管理合成事件对象的创建和销毁,这样减少了垃圾的生成和新对象内存的分配,大大提高了性能
事件注册
JSX中声明一个React事件十分简单,比如:
render() {
return (
<div onClick = {
(event) => {console.log(JSON.stringify(event))}
}
/>
);
}
它是如何注册到react事件系统中的呢?
从组件创建和更新的入口方法mountComponent和updateComponent说起。在这两个方法中,都会调用到_updateDOMProperties方法,对JSX中声明的组件属性进行处理。源码如下:
ReactDomComponent.js
_updateDOMProperties: function (lastProps, nextProps, transaction) {
....
} else if (registrationNameModules.hasOwnProperty(propKey)) {
if (nextProp) {
// enqueuePutListener注册事件
enqueuePutListener(this, propKey, nextProp, transaction);
} else if (lastProp) {
deleteListener(this, propKey);
}
}
enqueuePutListener,它负责注册JSX中声明的事件。源码如下:
// inst: React Component对象
// registrationName: React合成事件名,如onClick
// listener: React事件回调方法,如onClick=callback中的callback
// transaction: mountComponent或updateComponent所处的事务流中,React都是基于事务流的
function enqueuePutListener(inst, registrationName, listener, transaction) {
if (process.env.NODE_ENV !== 'production') {
// IE8 has no API for event capturing and the `onScroll` event doesn't
// bubble.
process.env.NODE_ENV !== 'production' ? warning(registrationName !== 'onScroll' ||
isEventSupported('scroll', true), 'This browser doesn\'t support the `onScroll` event')
: void 0;
}
var containerInfo = inst._nativeContainerInfo;
//documentElement
var doc = containerInfo._ownerDocument;
if (!doc) {
// Server rendering.
return;
}
// 注册事件,将事件注册到document上
listenTo(registrationName, doc);
// 存储事件,放入事务队列中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
enqueuePutListener主要做两件事,一方面将事件注册到document这个原生DOM上(这就是为什么只有document这个节点有DOM事件的原因),另一方面采用事务队列的方式调用putListener将注册的事件存储起来,以供事件触发时回调。
注册事件的入口是listenTo方法, 它解决了不同浏览器间捕获和冒泡不兼容的问题。事件回调方法在bubble阶段被触发。如果我们想让它在capture阶段触发,则需要在事件名上加上capture。比如onClick在bubble阶段触发,而onCaptureClick在capture阶段触发。listenTo代码虽然比较长,但逻辑很简单,调用ReactEventListener.js中trapCapturedEvent和trapBubbledEvent来注册捕获和冒泡事件。trapCapturedEvent大家可以自行分析,我们仅分析trapBubbledEvent,如下
// topLevelType: react的顶层事件类型 ’topClick‘
// handlerBaseName: 注册的事件类型 ’click‘
// handle: 监听事件的元素,document根元素(名字有点别扭)
trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
var element = handle;
if (!element) {
return null;
}
// 在html文档根元素上绑定事件,
// listent(documentElement, 'click', function(){});
// 底层就是调用addEventListener进行事件注册
return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},
事件回调函数的存储
事件存储由EventPluginHub来负责,它的入口在我们上面讲到的enqueuePutListener中的putListener方法,如下
ReactDomComponent.js
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
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);
}
/**
* Stores `listener` at `listenerBank[registrationName][id]`. Is idempotent.
* 利用事件名和react组件实例进行事件的存储
*/
// inst: 组件实例
// registrationName: 注册事件的名字 ’onClick‘
// listener: 声明的事件处理程序 function
putListener: function (inst, registrationName, listener) {
console.blue('putListener.js');
!(typeof listener === 'function') ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Expected %s listener to be a function, instead got type %s', registrationName, typeof listener) : invariant(false) : void 0;
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[inst._rootNodeID] = listener;
var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
},
事件存储在了listenerBank对象中,它按照事件名和React组件对象进行了二维划分,比如Test组件上注册的onClick事件最后存储在listenerBank.onClick[Test._rootNodeId]中。
事件执行
事件分发
当事件触发时,document上addEventListener注册的callback会被回调。从前面事件注册部分发现,此时回调函数为ReactEventListener.dispatchEvent,它是事件分发的入口方法。下面我们来详细分析
ReactEventListener.js
trapBubbledEvent: function (topLevelType, handlerBaseName, handle) {
var element = handle;
if (!element) {
return null;
}
// dispatchEvent函数为每个事件处理程序的入口
return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},
/*
* 每个事件被触发的时候,该函数都会首先调用
*/
// topLevelType: 'topClick'
// nativeEvent: 原生事件对象
dispatchEvent: function (topLevelType, nativeEvent) {
console.red('topLevelType', topLevelType);
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);
}
}
handleTopLevelImpl才是事件分发的真正执行者,它是事件分发的核心,体现了React事件分发的特点,如下
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;
console.blue('bookKeeping.ancestors');
do {
bookKeeping.ancestors.push(ancestor);
console.log('ancestor', 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));
}
}
事件的调用
var ReactEventEmitterMixin = {
/**
* 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完成。它其实是调用ReactBrowserEventEmitter.handleTopLevel() ,如下
handleTopLevel方法是事件callback调用的核心。它主要做两件事情,一方面利用浏览器回传的原生事件构造出React合成事件,另一方面采用队列的方式处理events。先看如何构造合成事件。
/**
* 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默认包含五种plugin,他们是在EventPluginHub初始化阶段注入进去的,且看代码
/**
* 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
});
不同的plugin针对不同的事件有特殊的处理,此处我们不展开讲了,下面仅分析SimpleEventPlugin中方法即可。
我们先看SimpleEventPlugin如何构造它所对应的React合成事件。
// 根据不同事件类型,比如click,focus构造不同的合成事件SyntheticEvent, 如SyntheticKeyboardEvent SyntheticFocusEvent
extractEvents: function (topLevelType, targetInst, nativeEvent, nativeEventTarget) {
var dispatchConfig = topLevelEventsToDispatchConfig[topLevelType];
if (!dispatchConfig) {
return null;
}
var EventConstructor;
// 根据事件类型,采用不同的SyntheticEvent来构造不同的合成事件
switch (topLevelType) {
... // 省略一些事件,我们仅以blur和focus为例
case 'topBlur':
case 'topFocus':
EventConstructor = SyntheticFocusEvent;
break;
... // 省略一些事件
}
// 从event对象池中取出合成事件对象,利用对象池思想,可以大大降低对象创建和销毁的时间,提高性能。这是React事件系统的一大亮点
var event = EventConstructor.getPooled(dispatchConfig, targetInst, nativeEvent, nativeEventTarget);
EventPropagators.accumulateTwoPhaseDispatches(event);
return event;
},
这里采用了event对象池特性,采用合成事件对象池的方式,可以大大降低销毁和创建合成事件带来的性能开销。
合成对象创建完成后,这里react自己模拟实现了捕获和冒泡两个阶段,
在该合成对象上添加两个重要内部属性:
1._dispatchListeners: 从触发事件的组件开始,向上级组件遍历,来存储注册的捕获和冒泡的事件处理程序
2._dispatchInstances: 从触发事件的组件开始,向上级组件遍历,来存储注册事件处理程序的该组件实例
EventPropagator.js
function accumulateTwoPhaseDispatches(events) {
console.blue('event eventPropagators', events);
console.log('events', events);
forEachAccumulated(events, accumulateTwoPhaseDispatchesSingle);
}
对事件队列里的每个事件,执行accumulateTwoPhaseDispatchesSingle
function accumulateTwoPhaseDispatchesSingle(event) {
console.log('event', event);
if (event && event.dispatchConfig.phasedRegistrationNames) {
EventPluginUtils.traverseTwoPhase(event._targetInst, accumulateDirectionalDispatches, event);
}
}
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);
console.log('inst', inst);
inst = inst._nativeParent;
}
var i;
for (i = path.length; i-- > 0;) {
fn(path[i], false, arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], true, arg);
}
}
traverseTwoPhase函数,模拟了捕获和冒泡两个阶段
traverseTwoPhase函数中的fn参数,就是EventPropagation.js中的accumulateDirectionalDispatches函数,用来向合成事件对象中累加对应组件实例的listener函数和相应实例
前面提到EventPluginHub.js putListener函数是用来存储listener,而获得相应组件实例上的注册的listener,用到了相应的getListener函数
getListener: function (inst, registrationName) {
var bankForRegistrationName = listenerBank[registrationName];
return bankForRegistrationName && bankForRegistrationName[inst._rootNodeID];
},
react以队列的形式处理合成事件。方法入口为runEventQueueInBatch,如下
ReactEventEmitterMixin.js
function runEventQueueInBatch(events) {
// 先将events事件放入队列中
EventPluginHub.enqueueEvents(events);
// 再处理队列中的事件,包括之前未处理完的。先入先处理原则
EventPluginHub.processEventQueue(false);
}
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);
}
!!eventQueue ? process.env.NODE_ENV !== 'production' ? invariant(false, 'processEventQueue(): Additional events were enqueued while processing ' + 'an event queue. Support for this has not yet been implemented.') : invariant(false) : void 0;
// This would be a good time to rethrow if any of the event handlers threw.
ReactErrorUtils.rethrowCaughtError();
},
合成事件处理也分为两步,先将我们要处理的events队列放入eventQueue中,因为之前可能就存在还没处理完的合成事件。然后再执行eventQueue中的事件。可见,如果之前有事件未处理完,这里就又有得到执行的机会了。
事件执行的入口方法为executeDispatchesAndReleaseTopLevel,如下
EventPluginHub.js
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);
}
}
};
function executeDispatchesInOrder(event, simulated) {
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._dispatchInstances;
console.blue('EventPluginUtils.js');
console.log('event._dispatchListeners', dispatchListeners);
console.log('event._dispatchInstances', dispatchInstances);
if (process.env.NODE_ENV !== 'production') {
validateEventDispatches(event);
}
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;
}
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
console.log('type', type);
console.log('currentTarget2', event.currentTarget);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
executeDispatchesInOrder会先得到event对应的listeners队列,然后执行listeners数组上的每一个listener函数
ReactErrorUtils.js
//listener函数的调用
// 采用func(a)的方式进行调用,
// 故ReactErrorUtils.invokeGuardedCallback(type, listener, event)最终调用的是listener(event)
// event对象为浏览器传递的DOM原生事件对象,这也就解释了为什么React合成事件回调中能拿到原生event的原因
function invokeGuardedCallback(name, func, a) {
try {
func(a);
} catch (x) {
if (caughtError === null) {
caughtError = x;
}
}
}
总结:
React事件系统还是相当麻烦的,主要分为事件注册,事件存储和事件执行三大部分。了解了React事件系统源码,就能够明白开头所列出的React事件几大特点了。