事件注册
在组件加载(mountComponent)、更新(updateComponent)的时候都需要进行事件代码的注册。
我们跳过前面这些无所谓的 Call Stack,直接看一下enqueuePutListener
在项目开发过程中,遇到了一个具体的需求,需求要求点击 ant-d的tree组件节点的时候,单击触发一种效果,双击触发另一种效果。 因为antd 没此功能,所以需要自己去实现。 具体原理就是250毫秒内再次点击,则认定为双击,doubleClick。开发过程中遇到一个问题。
clickNode = e => {
...
console.log(e.target); // 正常的target
this.timeId = setTimeout(() => {
console.log(e.target); // null
}, 250);
};
在setTimeout当中拿不到 e ,只有在回调的外部先存储想要的数据,内部才能访问。也就是说,在clickNode执行后,react 回收了e,想进一步了解react事件系统的执行,所以翻看了一下源码……
因为项目当中已经已经使用了dll进行打包,所以无法直接 debugger react的源码,且我有懒得在node_modules外装,灵机一闪,想到了自己多年年前搞的 一个菜鸟级脚手架,刚好可以胜任本次行动,所以默默的打开了自己的github,附地址:https://github.com/rongchanghai/react-Learner
脚手架中的react、react-dom 都是15.6.2,比较适合撸源码,并且当前react版本只是使用了Fiber重构了虚拟DOM的刷新机制,对事件系统没有什么影响。
我们都知道react没有采用原生JS的那套事件系统,而是自己实现了一套事件系统,这也是为什么我们在编码过程中,click 事件要写成 onClick 驼峰写法,而不是onclick的写法。并且react的事件系统是使用addEventLienster 将事件代理到document上。整体提升效率。
react事件系统总体分为两个部分
我们一步一步来看
ps:我们看源码不需要每行代码都看,有些可以直接略过,我们只关心主逻辑的代码。所以有一些我们就直接略过了。
整体的代码很简单
class Event extends React.Component {
componentDidMount(){
document.addEventListener('click', (e) => {
console.log('document!!!');
}, false);
}
innerClick(e) {
console.log('A: react inner click.');
}
outerClick(e) {
console.log('B: react outer click.');
}
render() {
return (
);
}
}
在组件加载(mountComponent)、更新(updateComponent)的时候都需要进行事件代码的注册。
我们跳过前面这些无所谓的 Call Stack,直接看一下enqueuePutListener
/**
* @param {*} inst -> ReactDOMComponent
* @param {*} registrationName -> "onClick"
* @param {*} listener -> cb
* @param {*} transaction -> ReactReconcileTransaction react的事务调度器
* @returns
*/
function enqueuePutListener(inst, registrationName, listener, transaction) {
// 得到 document,因为要将所有事件注册到document上去
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
});
}
在这段代码很简单,先获取到真实节点,判断节点是否是document,如果不是,则将其节点的顶层document以及事件名传入listenTo方法中进行注册。
listenTo
function listenTo(registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
// dependencies 为 topClick
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;
}
}
}
EventPluginRegistry.registrationNameDependencies 是一个map,所有的方法名,都会转成top开头的名称,不用管为什么。这只是react内部实现的一个叫法而已。
对于我们的这个topClick方法,核心的方法是 trapBubbledEvent。再深入看一下此方法。
trapBubbledEvent: function(topLevelType, handlerBaseName, element) {
if (!element) {
return null;
}
return EventListener.listen(
element,
handlerBaseName,
//事件回调
ReactEventListener.dispatchEvent.bind(null, topLevelType),
);
}
这个方法只是调用了 EventListener.listen 对事件进行绑定,再看一下此方法
var EventListener = {
/ **
*在冒泡阶段监听DOM事件。
*
* @param {DOMEventTarget}目标DOM元素,用于注册侦听器。
* @param {string} eventType事件类型,例如 “点击”或“鼠标悬停”。
* @param {function}回调回调函数。
* @return {object}具有`remove`方法的对象。
* /
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);
}
};
}
}
这个方法大家就能看懂了吧,就是一个正常的DOM事件绑定,做了一下兼容处理。(话说现在框架用多了,一些兼容方法都快忘了。再也不考虑IE 678了)。 但是我们的callback 比较特殊。不是我们普通的cb,而是一个加强版的。从trapBubbledEvent 中得知是 ReactEventListener.dispatchEvent.bind(null, topLevelType)。
dispatchEvent: function (topLevelType, nativeEvent) {
// nativeEvent 就是我们的原生event 对象,
...handleTopLevelImpl
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping);
...
}
上面的代码在事件触发阶段再讲。
我们会发现一个事情,就是说,我们绑定一个事件,我们在document上绑定的都是统一的回调函数handleTopLevelImpl,so?真正的事件回调在哪里呢???
listenTo 这条线完事了,退出来接着往下走。
function enqueuePutListener(inst, registrationName, listener, transaction) {
...
listenTo(registrationName, doc);
transaction.getReactMountReady().enqueue(putListener, {
inst: inst,
registrationName: registrationName,
listener: listener
});
CallbackQueue.prototype.enqueue = function enqueue(callback, context) {
console.log(callback);
this._callbacks = this._callbacks || [];
this._callbacks.push(callback);
this._contexts = this._contexts || [];
this._contexts.push(context);
};
这个简单就不解释了
加下来就是事件的存储操作。
注意我们用到了putListener 方法,putListener 中使用了EventPluginHub.putListener
var EventPluginHub = {
...
/ **
* 将 listener 存储在“ listenerBank [registrationName] [key]”上。 来一波幂等。
*
* @param {object} inst实例,事件的来源。
* @param {string} registrationName侦听器的名称(例如`onClick`)。
* @param {function}监听器要存储的回调。
* /
putListener: function (inst, registrationName, listener) {
// 这个key很重要。是我们的node Id
var key = getDictionaryKey(inst);
var bankForRegistrationName = listenerBank[registrationName] || (listenerBank[registrationName] = {});
bankForRegistrationName[key] = listener;
// 以上,将DOM 和 cb 绑定到 放到 listenerBank中,建立起对应关系
console.log('bankForRegistrationName', bankForRegistrationName);
// 下面的代码是做的一个兼容,增加了一个空方法的绑定,解决safair上无法触发冒泡事件的问题。
var PluginModule = EventPluginRegistry.registrationNameModules[registrationName];
if (PluginModule && PluginModule.didPutListener) {
PluginModule.didPutListener(inst, registrationName, listener);
}
}
至此,事件注册阶段基本就完事了,我们回顾一下:
所以我们的事件注册阶段是分为两块的: document注册和事件存储
上面我们有提到一个方法,handleTopLevelImpl,绑定的方法。现在可以看一下这个方法是干什么用的了
function handleTopLevelImpl(bookKeeping) {
console.log('事件触发');
// 获取一下真实的DOM 节点,这里特殊点是 如果是文本的话,就返回其父节点,这里涉及到nodeType的知识,1、2、3等分别代元素、属性、文本,长久不用这个知识点都有点忘了。哈哈
var nativeEventTarget = getEventTarget(bookKeeping.nativeEvent);
// 获得对应的ReactComponent
var targetInst = ReactDOMComponentTree.getClosestInstanceFromNode(nativeEventTarget);
//遍历层次结构,以防存在任何嵌套的组件。
//重要的是我们在调用任何祖先之前先建立祖先数组
//事件处理程序,因为事件处理程序可以修改DOM,从而导致
//与ReactMount的节点缓存不一致。
// 这波操作让我想起了redux中的一些逻辑。。。算了,想了解的看我之前的文章吧
var ancestor = targetInst;
do {
bookKeeping.ancestors.push(ancestor);
ancestor = ancestor && findParent(ancestor);
console.log('ancestor', ancestor);
} while (ancestor);
// 循环执行cb,模拟冒泡操作
for (var i = 0; i < bookKeeping.ancestors.length; i++) {
targetInst = bookKeeping.ancestors[i];
ReactEventListener._handleTopLevel(bookKeeping.topLevelType, targetInst, bookKeeping.nativeEvent, getEventTarget(bookKeeping.nativeEvent));
}
}
在看 ReactEventListener._handleTopLevel 的代码
handleTopLevel: function handleTopLevel(topLevelType, targetInst, nativeEvent, nativeEventTarget) {
debugger;
// 合成事件对象
var events = EventPluginHub.extractEvents(topLevelType, targetInst, nativeEvent, nativeEventTarget);
// 批量处理
runEventQueueInBatch(events);
}
function runEventQueueInBatch(events) {
EventPluginHub.enqueueEvents(events);
EventPluginHub.processEventQueue(false);
}
且看这代码
extractEvents: function extractEvents(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);
// 就是一个push 和 cancat的操作
if (extractedEvents) {
events = accumulateInto(events, extractedEvents);
}
}
}
// 返回一个事件对象数组
return events;
}
上述代码就是在合成事件的时候,会依次通过EventPluginRegistry.plugins插件列表来生成对应的事件数组,最后将这个生成的事件合并为一个数组返回。具体这些plugins,会根据不同的事件生成不同的事件对象。
对于不同的事件,React将使用不同的功能插件,这些插件都是通过依赖注入的方式进入内部使用的。React合成事件的过程非常繁琐,但可以概括出extractEvents函数内部主要是通过switch函数区分事件类型并调用不同的插件进行处理从而生成SyntheticEvent实例
接着看代码,
//触发该事件队列中的所有事件 EventPluginHub.processEventQueue(false);
processEventQueue: function(simulated) {
// 获取已合成事件队列
var processingEventQueue = eventQueue;
eventQueue = null;
if (simulated) {
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseSimulated,
);
} else {
// 让所有事件执行executeDispatchesAndReleaseTopLevel方法,
forEachAccumulated(
processingEventQueue,
executeDispatchesAndReleaseTopLevel,
);
}
ReactErrorUtils.rethrowCaughtError();
}
在执行 forEachAccumulated 之后又依次执行了
我们看一下 executeDispatchesInOrder 方法
function executeDispatchesInOrder(event, simulated) {
// debugger;
// reactDom 和 回调函数
var dispatchListeners = event._dispatchListeners;
var dispatchInstances = event._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';
// 获取实际dom
event.currentTarget = EventPluginUtils.getNodeFromInstance(inst);
if (simulated) {
ReactErrorUtils.invokeGuardedCallbackWithCatch(type, listener, event);
} else {
// 执行函数,
ReactErrorUtils.invokeGuardedCallback(type, listener, event);
}
event.currentTarget = null;
}
ReactErrorUtils.invokeGuardedCallback = function (name, func, a) {
var boundFunc = function boundFunc() {
// 执行回调方法
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);
};
到此,我们定义的回调函数就执行完了,但是要解决我最初的疑问,还得往下看。
var executeDispatchesAndRelease = function executeDispatchesAndRelease(event, simulated) {
if (event) {
EventPluginUtils.executeDispatchesInOrder(event, simulated);
if (!event.isPersistent()) {
// 释放 event
event.constructor.release(event);
}
}
};
var standardReleaser = function standardReleaser(instance) {
debugger;
var Klass = this;
!(instance instanceof Klass) ? process.env.NODE_ENV !== 'production' ? invariant(false, 'Trying to release an instance into a pool of a different type.') : _prodInvariant('25') : void 0;
// 重要代码
instance.destructor();
if (Klass.instancePool.length < Klass.poolSize) {
Klass.instancePool.push(instance);
}
};
因为执行完成之后,react会将合成的时间对象进行释放,所以我们在setTimeout 之后异步拿到的event 其实已经被释放掉了,所以会报错。
综上,react 事件系统代码就看了一遍,其中有些我认为不是特别重的部分,就没有分析。整体是按照call stack的顺序来一步步分析。目的是解决最开始的疑问和了解事件系统的实现。
其实react 之所以自己实现一波事件系统的主要目的还是为了提升执行效率,因为react不能控制用户coder的操作。如果有1000个DOM,都被绑定了各自的click事件,那执行起来效率就有点太低了。react 整体截获了所有的事件,然后将其挂载到document上统一进行管理,大幅提升性能,且处理了不同浏览器的兼容性。
收工,告辞~

愚墨
保持饥渴的专注,追求最佳的品质
文章评论