前言
上篇文章我们了解了React合成事件跟原生绑定事件是有区别的,本篇文章从源码来深挖一下React的事件机制。
TL;DR :
- react事件机制分为两个部分:1、事件注册 2、事件分发
- 事件注册部分,所有的事件都会注册到document上,拥有统一的回调函数dispatchEvent来执行事件分发
- 事件分发部分,首先生成合成事件,注意同一种事件类型只能生成一个合成事件Event,如onclick这个类型的事件,dom上所有带有通过jsx绑定的onClick的回调函数都会按顺序(冒泡或者捕获)会放到Event._dispatchListeners 这个数组里,后面依次执行它。
还是使用上次的栗子:
class ExampleApplication extends React.Component {
componentDidMount() {
document.addEventListener('click', () => {
alert('document click');
})
}
outClick(e) {
console.log(e.currentTarget);
alert('outClick');
}
onClick(e) {
console.log(e.currentTarget);
alert('onClick');
e.stopPropagation();
}
render() {
return <div onClick={this.outClick}>
<button onClick={this.onClick}> 测试click事件 </button>
</div>
}
}
分析源码之前,有些工作和知识要提前准备,普及一下:
- 请各位准备好一个编辑器,自行用react-starter-kit建一个react项目,复制上面的代码,渲染上面的组件,然后打开控制台
- 下图是整个事件机制的流程图,后面会分部分解析
https://www.processon.com/dia... - 普及几个功能函数,提前了解它的作用
// 作用:如果只是单个next,则直接返回,如果有数组,返回合成的数组,里面有个
//current.push.apply(current, next)可以学习一下,我查了一下[资料][3]https://jsperf.com/array-prototype-push-apply-vs-concat/2,这样组合数组比concat效率更高
// 栗子:input accumulateInto([],[])
function accumulateInto(current, next) {
if (current == null) {
return next;
}
// Both are not empty. Warning: Never call x.concat(y) when you are not
// certain that x is an Array (x could be a string with concat method).
if (Array.isArray(current)) {
if (Array.isArray(next)) {
current.push.apply(current, next);
return current;
}
current.push(next);
return current;
}
if (Array.isArray(next)) {
// A bit too dangerous to mutate `next`.
return [current].concat(next);
}
return [current, next];
}
// 这个其实就是用来执行函数的,当arr时数组的时候,arr里的每一个项都作为回调函数cb的参数执行;
// 如果不是数组,直接执行回调函数cb,参数为arr
// 例如:
// arr为数组:forEachAccumulated([1,2,3], (item) => {console.log(item), this})
// 此时会打印出 1,2,3
// arr不为数组,forEachAccumulated(1, (item) => {console.log(item), this})
// 此时会打印出 1
function forEachAccumulated(arr, cb, scope) {
if (Array.isArray(arr)) {
arr.forEach(cb, scope);
} else if (arr) {
cb.call(scope, arr);
}
}
React事件机制
React事件机制分为两块:
- 事件注册
- 事件分发
我们一步步来看:
事件注册
整个过程从ReactDomComponent
开始,重点在enqueuePutListener
,这个函数做了三件事情,详细请参考下面源码:
ReactDomComponent.js
function enqueuePutListener () {
// 省略部分代码
...
// 1、*重要:在这里取出button所在的document*
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
// 2、在document上注册事件,同一个事件类型只会被注册一次
listenTo(registrationName, doc);
// 3、mountReady之后将回调函数存在ListernBank中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
接下来看看第二步:在document上注册事件 的过程,流程图如下:
接着我们抽出每个文件的重点函数出来分析:
ReactBrowserEventEmitter.js
listenTo: function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
// 检测document上是否已经监听onClick事件,所以前面说同一类型事件只会绑定一次
var isListening = getListeningForDocument(mountAt);
// 获得dependency,将onClick 转成topClick,这只是一种处理方式不用纠结
var dependencies =
EventPluginRegistry.registrationNameDependencies[registrationName];
// 中间是对各种事件类型给document绑定捕获事件或者冒泡事件,大部分都是冒泡,
...
// 这里我们的topClick,绑定的是冒泡事件
else if (topEventMapping.hasOwnProperty(dependency)) {
// trapBubbledEvent会在下面分析
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
// 最后把topClick标记为已注册过,防止重复注册
isListening[dependency] = true;
}
由于onclick绑定的是冒泡事件,所以我们来看看trapBubbledEvent
ReactEventListener.js
// 输入: topClick, click, doc
trapBubbledEvent: function (topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
// EventListener 要做的事情就是把事件绑定到document上,注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent
return EventListener.listen(element, handlerBaseName, ReactEventListener.dispatchEvent.bind(null, topLevelType));
},
// EventListener.js
// 输入doc, click, dispatchEvent
// 这个函数其实就是我们熟悉的兼容浏IE浏览器事件绑定的方法
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);
}
};
}
},
注意这里无论是注册冒泡还是捕获事件,最终的回调函数都是dispatchEvent,所以我们来看看dispatchEvent
怎么处理事件分发。
dispatchEvent
看到这里大家会奇怪,所有的事件的回调函数都是dispatchEvent
来处理,那事件onClick
原来的回调函数存到哪里去了呢?
再回来看事件注册的第三步:mountReady之后将回调函数存在ListernBank中
ReactDomComponent.js
function enqueuePutListener () {
// 省略部分代码
...
// 1、*重要:在这里取出button所在的document*
var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument;
// 2、在document上注册事件,同一个事件类型只会被注册一次
listenTo(registrationName, doc);
// 3、mountReady之后将回调函数存在ListernBank中
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
}
在document
上注册完所有的事件之后,还需要把listener
放到listenerBank
中以listenerBank[registrationName][key]
这样的形式存起来,然后在dispatchEvent
里面使用。
将listener放到listenerBank中储存的过程如下:
ReactDomComponent.js
// 在putListener里存入listener
function putListener() {
var listenerToPut = this;
// 先put的是外层的listener - outClick,所以这里的inst是外层div
// registrationName是onclick,listener是outClick
EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener);
}
EventPluginHub.js
/**
* 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); // 先根据inst得到唯一的key
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
// 可以看到最终listener 在 listenerBank里,最终以listenerBank[registrationName][key] 存在
bankForRegistrationName[key] = listener;
var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
// 这里的didPutListener只是为了兼容手机safari对non-interactive元素
// 双击响应不正确,详情可以参考这篇[文章][7]
//https://www.quirksmode.org/blog/archives/2010/09/click_event_del.html
PluginModule.didPutListener(inst, registrationName, listener);
}
},
以上就是事件注册的过程,接下来在看dispatchEvent如何处理事件分发。
事件分发
在介绍事件分发之前,有必要先介绍一下生成合成事件的过程,链接是https://segmentfault.com/a/11...
了解合成事件生成的过程之后,我们需要get一个点:合成事件收集了一波同类型(例如click
)的回调函数存在了合成事件event._dispatchListeners
这个数组里,然后将它们事件对应的虚拟dom节点放到_dispatchInstances
就本例来说,_dispatchListeners= [onClick, outClick]
,之后在一起执行。
接下来看看事件分发的过程:
EventListener.js
dispatchEvent: function (topLevelType, nativeEvent) {
if (!ReactEventListener._enabled) {
return;
}
// 这里得到TopLevelCallbackBookKeeping的实例对象,本例中第一次触发dispatchEvent时
// bookKeeping = {ancestors: [],nativeEvent,‘topClick’}
var bookKeeping = TopLevelCallbackBookKeeping.getPooled(topLevelType, nativeEvent);
try {
// Event queue being processed in the same cycle allows
// `preventDefault`.
// 接着执行handleTopLevelImpl(bookKeeping)
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
}
function handleTopLevelImpl(bookKeeping) {
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 获取当前事件的虚拟dom元素
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
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];
// 这里的_handleTopLevel 对应的就是ReactEventEmitterMixin.js里的handleTopLevel
ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}
// 这里的findParent曾经给我带来误导,我以为去找当前元素所有的父节点,但其实不是的,
// 我们知道一般情况下,我们的组件最后会被包裹在<div id='root'></div>的标签里
// 一般是没有组件再去嵌套它的,所以通常返回null
/**
* 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) {
while (inst._hostParent) {
inst = inst._hostParent;
}
var rootNode = ReactDOMComponentTree.getNodeFromInstance(inst);
var container = rootNode.parentNode;
return ReactDOMComponentTree.getClosestInstanceFromNode(container);
}
上面这段代码的重点就是_handleTopLevel
,它可以获取合成事件,并且去执行它。
下面看看具体是如何执行:
ReactEventEmitterMixin.js
function runEventQueueInBatch(events) {
// 1、先将事件放进队列里
EventPluginHub.enqueueEvents(events);
// 2、执行它
EventPluginHub.processEventQueue(false);
}
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) {
// 用EventPluginHub生成合成事件
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 执行合成事件
runEventQueueInBatch(events);
}
};
执行的过程分成两步:
- 将事件放进队列
- 执行
执行的细节如下:
EventPluginHub.js
var executeDispatchesAndReleaseTopLevel = function (e) {
return executeDispatchesAndRelease(e, false);
};
var executeDispatchesAndRelease = function (event, simulated) {
if (event) {
// 在这里dispatch事件
EventPluginUtils.executeDispatchesInOrder(event, simulated);
// 释放事件
if (!event.isPersistent()) {
event.constructor.release(event);
}
}
};
enqueueEvents: function (events) {
if (events) {
eventQueue = accumulateInto(eventQueue, events);
}
},
/**
* 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 fexers threw.
ReactErrorUtils.rethrowCaughtError();
},
上段代码里,我们最终会走到
forEachAccumulated(processingEventQueue, executeDispatchesAndReleaseTopLevel);
forEachAccumulated
这个函数我们之前讲过,就是对数组processingEventQueue的每一个合成事件都使用executeDispatchesAndReleaseTopLevel
来dispatch 事件。
所以各位同学们,注意到这里我们已经走到最核心的部分,dispatch 合成事件了,下面看看dispatch的详细过程:
EventPluginUtils.js
/**
* 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++) {
// 由这里可以看出,合成事件的stopPropagation只能阻止react合成事件的冒泡,
// 因为event._dispatchListeners 只记录了由jsx绑定的绑定的事件,对于原生绑定的是没有记录的
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 合成事件分为两个步骤:
- 通过_dispatchListeners里得到所有绑定的回调函数,在通过_dispatchInstances的绑定回调函数的虚拟dom元素
- 循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理
当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就做到了阻止事件冒泡
目前还是还有看到执行事件的代码,在接着看:
EventPluginHub.js
function executeDispatch(event, simulated, listener, inst) {
var type = event.type || 'unknown-event';
// 注意这里将事件对应的dom元素绑定到了currentTarget上
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
// 一般都是非模拟的情况,执行invokeGuardedCallback
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
上面这个函数最重要的功能就是将事件对应的dom元素绑定到了currentTarget上,
这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。
下面就是整个执行过程的尾声了:
ReactErrorUtils.js
var fakeNode = document.createElement('react');
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
var boundFunc = function () {
func(a);
};
var evtType = 'react-' + name;
fakeNode.addEventListener(evtType, boundFunc, false);
var evt = document.createEvent('Event');
evt.initEvent(evtType, false, false);
fakeNode.dispatchEvent(evt);
fakeNode.removeEventListener(evtType, boundFunc, false);
};
由invokeGuardedCallback
可知,最后react调用了faked元素的dispatchEvent方法来触发事件,并且触发完毕之后立即移除监听事件。
总的来说,整个click事件被分发的过程就是:
1、用EventPluginHub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchListeners里储存了同一事件类型的所有回调函数
2、按顺序去执行它
就辣么简单!
本文比较长,有不理解的欢迎提问~ 或者有理解错误的也请大家指正。
最后附上整个流程图文件:
s://segmentfault.com/a/1190000013343819
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。