事件注册
在组件加载(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上统一进行管理,大幅提升性能,且处理了不同浏览器的兼容性。
收工,告辞~
愚墨
保持饥渴的专注,追求最佳的品质
文章评论