写在最前
这里暂时不做高屋建瓴式的解读直接给出关键文件。而是从之前已有的分析开始,尝试自己蛛丝马迹找出来。
既然是事件绑定,那么必然在ReactDom.render
的环节里面有处理。所以这里简单回顾然后把它挖出来。
我们知道React在浏览器的运行时里面的渲染存在两种情况,一种是第一次的初次渲染,一种是props更新带来的更新。
当我们说事件的时候,我们是在说谁的事件?之前分析已经提到了调度函数ReactReconciler返回的几种实例:ReactDOMEmptyComponent
、ReactDOMComponent
实例、ReactCompositeComponentWrapper
实例。
那么回想一下,当我们绑定事件的时候第一次渲染里面必然会有,props和state更新时候我们的React事务也会这样做(清场和现场还原)。
这个操作呢,第一次渲染时候大抵在ReactCompositeComponentWrapper.mountComponent
里面,它可能是这个组件上绑定,再不济也应该是在其children中绑定,后面的渲染,则大抵在ReactUpdates.js
的ReactUpdatesFlushTransaction
中。
不过整体按React抽象至死的尿性,显然最后都会殊途同归。不过这里还是尝试整理一下两条路线下的调用路径。
mountComponent路径
这里不再重复贴代码,如果读到这里的时候没有看过之前的分析,也没有自己打开IDE debug过,其实这篇文章读与不读,本质上区别不大。
这个函数代码不多,配合这个函数的注释简直确认了我自己猜测的注释:
1 | /** |
简单分析ReactCompositeComponent.mountComponent
可以很容易知道这里的事件绑定,下一步路径是ReactCompositeComponentWrapper.performInitialMount
。
在这个函数内
1 | var child = this._instantiateReactComponent(renderedElement, nodeType !== ReactNodeTypes.EMPTY /* shouldHaveDebugID */ |
child是自定义函数里面的render里面的子组件,他的type是一个string,这样调度模块使用ReactDOMComponent.mountComponent来定义这块。
回顾一下ReactDOMComponent._updateDOMProperties
里面提到的enqueuePutListener
,这里是事件注册的关注点。这个函数里面进行绑定的关键代码:
1 | listenTo(registrationName, doc); |
这里事件绑定到document上,然后使用putListener来进行事件绑定。
其中listenTo
指向ReactBrowserEventEmitter.listenTo
,putListener
指向EventPluginHub.putListener
。
props更新路径
参考之前lifecycle里面props更新环节的render分析和ReactCompositeComponent._updateRenderedComponent
分析。这里的定位分析点是起始点ReactCompositeComponent._updateRenderedComponent
。
这里有替换和更新两个分支 我们这里只谈更新。
1 | ReactReconciler.receiveComponent( |
和之前同样的道理,定位到ReactDOMComponent.receiveComponent
。一路顺着
1 | ReactDOMComponent.receiveComponent |
的路径,显然,最初的猜测一点没错。
补充 & 旁白
这里两个路径的探索,说白了还是从断点角度的摸索,属于从下往上的摸索。虽然可以摸清楚这些,但是实际上不是最好的办法。
但是如果可以对虚拟DOM节点、render、和Diff路径有相对清晰的了解和认知,那么当可以从顶层设计上对这个ReactDOMComponent._updateDOMProperties
函数进行定位。
如果非要一个思路,这里可以这样思考:
- Diff算法实质上除了’移动’,不存在实质意义上的’更新’,这个更新本质是删除旧的+替换新的。
- 不管是初始render还是替换,都需要对一个新的ReactDomCompositeComponent进行实例化备用。
- 这个过程中终究会伴随
_updateDOMProperties
函数调用。 - 这个
_updateDOMProperties
函数,是自定义组件渲染为ReactDOMComponent的核心调用。
正式开始
事件保存 && 绑定
保存
事件保存主要是走的EventPluginHub.putListener
,将putListener保存到了listenerBank。具体一点来说,是将事件放到了listenerBank[registrationName][getDictionaryKey(inst)]
。这里getDictionaryKey
的返回值是'.' + inst._rootNodeID
。
绑定
来自ReacDomComponent.js文件:
1 | function enqueuePutListener(inst, registrationName, listener, transaction) { |
这个绑定是由listenTo(registrationName, doc)
处理的,直接把事件绑定到document,利用事件冒泡性质来进行事件委托处理。这个listenTo实际上就是ReactBrowserEventEmitter.listenTo
。
事件触发 && 分发
事件触发这个实际上在上一小结就提到了,当事件在某个元素上触发,会冒泡到document,然后document开始触发相关逻辑。
事件分发主要是通过ReactBrowserEventEmitter.listenTo
处理。这里关于浏览器兼容有点多,不过这里管住核心代码就好。这里的listenTo
有两个核心调用, 都在ReactBrowserEventEmitter.ReactEventListener
上。
ReactEventListener
- trapBubbledEvent
- trapCapturedEvent
看看它们引用到的listen和capture中的listen(React 15.6是支持冒泡+捕获两种事件传播的,具体参考traverseTwoPhase函数,但是这仅仅是传播的支持,并没有具体实现这个捕获事件触发)。这里就是我们熟悉的原生API了。
1 | listen: function (target, eventType, callback) { |
这里注意的是我们这里的addEventListener是在DOM层级上触发的,当我们触发了这个事件,接下来需要找到我们保存的回调函数,不然这个触发就毫无意义。
以trapBubbledEvent
为例:
1 | trapBubbledEvent: function(topLevelType, handlerBaseName, element) { |
这里传入到listen中的事件回调是ReactEventListener.dispatchEvent.bind(null, topLevelType)
首先,这里有个十分重要的地方需要注意,那就是这个element
变量。这个element常规情况下直接指向的document对象。也就是说,事件绑定在document上了。这可以方便整体的事件管理,原理同大家用过的Jquery事件委托。
其次,这里进一步看看ReactEventListener.dispatchEvent
的基础定义:
1 | dispatchEvent: function (topLevelType, nativeEvent) { |
EventListener.listen
本质上还是addEventListener
的封装,所以作为回调的函数ReactEventListener.dispatchEvent.bind(null, topLevelType)
它会被传入一个event事件对象 ,这个对象会被作为nativeEvent参数传入到TopLevelCallbackBookKeeping
。然后就是重点了: event
对象中有触发的DOM node节点引用,这点非常重要,只有如此,我们才可以进一步在虚拟DOM树中获取到需要触发冒泡捕获的所有节点,这点同样非常重要。
PS: 除了这里常规的事件委托,关于SimpleEventPlugin.putListener
可以了解一下。
dispatchEvent
ReactEventListener.dispatchEvent
这里做的事情显然是触发我们定义好的回调,我们所有的事件绑定的回调函数都被储存在一个集中的地方,那么我们现在看看它如何从这个集合中正确找出我们需要的事件回调。
1 | dispatchEvent: function(topLevelType, nativeEvent) { |
关于ReactUpdates.batchedUpdates
之前在render相关文章里面有分析,这里不做更多分析,这里的关键是handleTopLevelImpl
handleTopLevelImpl
1 | function handleTopLevelImpl(bookKeeping) { |
这里do~while
先缓存target触发事件那一瞬的所有父组件,这是一个数组,从左到右,右边的是左边的父组件。这里i从0起,其实也就是事实上走了从子到父的路径,也就是说,只有冒泡被实现了。这里_handleTopLevel
指向ReactEventEmitterMixin.handleTopLevel
。
1 | handleTopLevel: function( |
这里有extractEvents
从注册的事件里面将目标事件取出,然后调用runEventQueueInBatch
批量执行回调。其内容仍旧是EventPluginHub
上的:
1 | EventPluginHub.enqueueEvents(events); |
接下来看看这两个函数细节。enqueueEvents作用相对简单,维护EventPluginHub.eventQueue
变量确保其是一个事件组成的一维数组(也可能是null)。
processEventQueue函数则是触发所有事件,将事件清空,并抛出其运行时的错误。
1 |
|
这里executeDispatchesAndReleaseSimulated
、executeDispatchesAndReleaseTopLevel
都是调用的executeDispatchesAndRelease
只不过一个传入了true参数,一个传入了false参数。
一路深入这个函数的调用栈
1 | executeDispatchesAndRelease |
其实他们最终调用到的都是同一个函数invokeGuardedCallback
。这个函数作用就是直接执行eventHandle,并捕获错误,没别的作用了。
小总结
前面分析的都是具体的路径,不过总归对触发还是需要一个原理上的总结。
我们知道事件在DOM中的传播是先从外层逐层捕获,然后从里层逐层冒泡。这个过程涉及到的DOM层实际上就是触发事件的target node,和其上级的各个父祖元素。这是其一。
其二,当冒泡捕获发生以后,会在各个父祖元素上也触发对应的事件回调。
其三,当我们在document上触发事件时候,事件回调会被传入一个event对象,这个event对象里面会有触发事件的target node(HTMLElement)的引用,配合ReactDOMComponentTree的API或者其他,总之精确获取到要触发冒泡的目标组件集合就可以做到顺手拈来。
最后,触发的那瞬间在储存的事件集合里面找到对应inst._rootNodeID
的事件并触发即可。当然,这里的触发也会进行批处理优化(runEventQueueInBatch)。
这就是整体的原理。至于细节,可以慢慢从上面的分析上思考。
源码细节
EventPluginHub
API | 说明 |
---|---|
putListener | 保存listener到集合 |
getListener | 从集合中获取listener |
deleteListener | 从集合删除这个listener |
deleteAllListeners | 移除集合里面所有listener |
extractEvents | 允许已注册插件从浏览器原生事件获取已注册事件 |
enqueueEvents | 将事件排队推入一个合成事件中,在processEventQueue执行时触发 |
processEventQueue | 执行事件队列上的所有合成事件 |
extractEvents
extractEvents方法可能是整个流程中比较影响深入理解的,这里简单分析一下。
这个函数实质上遍历EventPluginRegistry.plugins
,然后通过根据其内部的plugin自身的extractEvents
方法来获取事件。以SimpleEventPlugin
为例。这里假设TopLevelTypes === ‘topError’
。此时这个代码的逻辑就相当于:
1 | var event = SyntheticEvent( |
此时event的数据结构是这样的
EventPropagators.accumulateTwoPhaseDispatches(event)
顺藤摸瓜则指向了:
1 | EventPluginUtils.traverseTwoPhase( |
其最终的指向则是ReactDOMTreeTraversal.traverseTwoPhase
,这个函数模拟冒泡/捕获环节的事件分发的遍历行为,实质上它是获取所有父元素得到数组(索引升序对应更上层的父元素),先降序遍历触发捕获回调,然后升序遍历触发冒泡回调的过程。而这个回调则指向了accumulateDirectionalDispatches
。
1 | function accumulateDirectionalDispatches(inst, phase, event) { |
这个函数整体来说做了两件事就是操作event._dispatchListeners && event._dispatchInstances
。
在之前我们提到traverseTwoPhase
是冒泡捕获的遍历过程的模拟,对应的说这里的phase
参数就是这个过程的标志,captured
和bubbled
是其可选值。之所以有这个参数 就是因为在不同阶段对应的listener并不一样。**listenerAtPhase
实质上调用EventPluginHub.getListener
来获取这个listener**。
其他
除了这些,这里还有一个injection方法,通过EventPluginRegistry
上的方法来进行一些依赖注入。
这个注入还是在之前见过很多次的ReactDefaultInjection.js
里面。这里整体代码是这样的,为了方便阅读有简单修改。
1 | ReactInjection.EventPluginHub.injectEventPluginOrder([ |
EventPluginRegistry
EventPluginHub是一个Hub,更多时候它只是一个调度。而EventPluginRegistry,是EventPlugin实际上的数据源,上面提到的注入实际上还是注入到这里,所以还是看看里面东西。
API | 说明 |
---|---|
injectEventPluginOrder | 注入 插件排序的次序表 这里仅仅是插件名称(Array) |
injectEventPluginsByName | 注入 插件本体 这个必须在次序表中 |
getPluginModuleForEvent | 查找提供的事件的plugin |
_resetEventPlugins | 单元测试专用API |
总结&思考
关于React的事件系统实际上源码里面(ReactBrowserEventEmitter
)有注释,并且还有一个流程图示
1 | /** |
这里暂时还有很多细节没有被理解:
- 合成事件概念 && 事件定义的数据结构
- EventPluginHub && EventPluginRegistry 联系
合成事件 && 其数据结构
React定义了合成事件,它在顶层API上,对不同浏览器进行了兼容处理。它里面和原生事件一样有preventDefault && preventDefault
。
这里仅就最简单的SyntheticEvent
做分析,其他的鼠标、滚动、拖曳之类的合成事件都是对它的拓展、继承。
这个合成事件的定义主要是三个地方。
1 | var EventInterface = { |
整体的属性、方法如下表。最有可能影响理解的可能是对原生event对象属性的基础上。仔细看看可以看到合成事件大部分都是在模拟原生Event对象的属性和方法。
属性、方法名 | 类型 | 说明 |
---|---|---|
dispatchConfig | ||
_targetInst | ReactDomComponent实例或其他实例 | ReactNode实例 |
nativeEvent | DOMEvent | 原生event对象 |
type | string | type |
target | DOMEventTarget | target |
currentTarget | DOMEventTarget | currentTarget |
eventPhase | number | event.eventPhase |
bubbles | boolean | bubbles |
cancelable | boolean | cancelable |
timeStamp | number | timeStamp |
defaultPrevented | boolean | defaultPrevented |
isTrusted | boolean | isTrusted |
preventDefault | Function | 阻止浏览器默认行为 |
stopPropagation | Function | 停止冒泡 |
persist | Function | 阻止将其放入对象池 保持其引用 |
isPersistent | boolean | 检查是否应将此事件释放回对象池中 |
destructor | Function | PooledClass会用到它 |
这里dispatchConfig的大致结构如下, 关于这个数据结构SimpleEventPlugin.js
中eventTypes
变量有注释和代码定义。
1 | { |
简单来说,onClick注册的事件会在冒泡环节触发,如果要在捕获环节触发,那么使用onClickCapture进行注册。这里phasedRegistrationNames
还是很容易理解。而dependencies
则是被依赖的top事件组成的数组,在冒泡捕获触发环节所有的事件诸如提到的onClick、onClickCapture
最后都会被转换为topClick
这样的形式进行触发。
EventPluginHub && EventPluginRegistry 联系
这里EventPluginHub是建立在EventPluginRegistry上的,从理解上,个人认为EventPluginRegistry实际上是处理的依赖注入的事情。他将plugin名称和本体保存起来,以供EventPluginHub方便调用。
三张图
想了一下,虽然自己最后分析了这么多,但是太过于重视细枝末节其实很难把握宏观上的处理方式。这源码就算读破了天,最终如果没有建立起体系其实没有什么卵用,所以这里画张图。
不过合成事件的各种继承,其实应该也值得很好研究一下,这里暂时不去细读这一块了。