上篇文章咱们了解了React合成事件跟原生绑定事件是有区别的,本篇文章从源码来深挖一下React的事件机制。html
TL;DR :node
仍是使用上次的栗子:react
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> } }
分析源码以前,有些工做和知识要提早准备,普及一下:git
// 做用:若是只是单个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事件机制分为两块:github
咱们一步步来看:web
整个过程从ReactDomComponent
开始,重点在enqueuePutListener
,这个函数作了三件事情,详细请参考下面源码:segmentfault
function enqueuePutListener () { // 省略部分代码 ... // 一、*重要:在这里取出button所在的document* var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 二、在document上注册事件,同一个事件类型只会被注册一次 listenTo(registrationName, doc); // 三、mountReady以后将回调函数存在ListernBank中 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); }
接下来看看第二步:在document上注册事件 的过程,流程图以下:数组
接着咱们抽出每一个文件的重点函数出来分析:浏览器
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
app
// 输入: 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
来处理,那事件onClick
原来的回调函数存到哪里去了呢?
再回来看事件注册的第三步:mountReady以后将回调函数存在ListernBank中
function enqueuePutListener () { // 省略部分代码 ... // 一、*重要:在这里取出button所在的document* var doc = isDocumentFragment ? containerInfo._node : containerInfo._ownerDocument; // 二、在document上注册事件,同一个事件类型只会被注册一次 listenTo(registrationName, doc); // 三、mountReady以后将回调函数存在ListernBank中 transaction.getReactMountReady().enqueue(putListener, { inst: inst, registrationName: registrationName, listener: listener }); }
在document
上注册完全部的事件以后,还须要把listener
放到listenerBank
中以listenerBank[registrationName][key]
这样的形式存起来,而后在dispatchEvent
里面使用。
将listener放到listenerBank中储存的过程以下:
// 在putListener里存入listener function putListener() { var listenerToPut = this; // 先put的是外层的listener - outClick,因此这里的inst是外层div // registrationName是onclick,listener是outClick EventPluginHub.putListener(listenerToPut.inst, listenerToPut.registrationName, listenerToPut.listener); }
/** * 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]
,以后在一块儿执行。
接下来看看事件分发的过程:
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
,它能够获取合成事件,而且去执行它。
下面看看具体是如何执行:
function runEventQueueInBatch(events) { // 一、先将事件放进队列里 EventPluginHub.enqueueEvents(events); // 二、执行它 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); } };
执行的过程分红两步:
执行的细节以下:
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的详细过程:
/** * 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 合成事件分为两个步骤:
当回调函数里使用了stopPropagation会使得数组后面的回调函数不能执行,这样就作到了阻止事件冒泡
目前仍是还有看到执行事件的代码,在接着看:
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元素。
下面就是整个执行过程的尾声了:
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事件被分发的过程就是:
一、用EventPluginHub生成合成事件,这里注意同一事件类型只会生成一个合成事件,里面的_dispatchListeners里储存了同一事件类型的全部回调函数
二、按顺序去执行它
就辣么简单!
本文比较长,有不理解的欢迎提问~ 或者有理解错误的也请你们指正。
最后附上整个流程图文件:
s://segmentfault.com/a/1190000013343819