本文通过一个简短的实例&控制台调试,了解react
事件处理的全过程。下面是测试用代码,使用控制台可以清晰看到函数执行过程中参数变化以及方法所属模块&调用栈,所以本文图片较多。
class RemoveBtn extends Component {
clickHandler = () => {
this.props.handleClick();
}
render(){
return(
<button onClick={this.clickHandler}>togglage测试组件</button>
)
}
}
class Root extends Component {
clickHandler = () => {
alert('hanlder is1 perform')
}
render(){
return (
<div className="first">
<RemoveBtn handleClick = {this.clickHandler}/>
</div>
)
}
}
ReactDOM.render(<Root />, document.getElementById('root'));
1 事件绑定
1.1 绑定的结果
说明: 这里的backend.js
是react调试工具的脚本不用考虑。
图中可见只有在document
上绑定了名为dispatchEvent
的来自于 ReactEventListener.js
模块的事件处理函数。
1.2 事件绑定的过程
ReactDOM.render(<Root />, document.getElementById('root'));
一切开始于ReactDOM.render
调用的ReactMount.js
的render
方法。忽略掉实例化组建的过程,详细调用可以查看截图右侧的调用栈。
_renderSubtreeIntoContainer
-> mountComponentIntoNode
-> mountComponent
[reactReconciler.js] -> _updateDOMProperties
_updateDOMProperties
函数在mountComponent
,unmountComponent
和updateComponent
阶段都有调用,它是检查属性变化,调优性能的重要方法。下图节选处理事件绑定部分代码,方法中有指向上次属性值得lastProp
, nextProp
是当前属性值,这里nextProp
是我们绑定给组件的onclick
事件处理函数。nextProp
不为空调用enqueuePutListener
绑定事件为空则注销事件绑定。
enqueuePutListener
这个方法只在浏览器环境下执行,传给listenTo参数分别是事件名称'onclick'和代理事件的绑定dom。如果是fragement
就是根节点(在reactDom.render指定的),不是的话就是document
。listenTo
用于绑定事件到 document ,下面交由事务处理的是回调函数的存储,便于调用。ReactBrowserEventEmitter
文件中的 listenTo
看做事件处理的源头。
listenTo: function (registrationName, contentDocumentHandle) {
var mountAt = contentDocumentHandle;
var isListening = getListeningForDocument(mountAt);
// 获取 registrationName(注册事件名称)的topLevelEvent(顶级事件类型)
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)) {
// 获取 topLevelEvent 对应的浏览器原生事件
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent(dependency, topEventMapping[dependency], mountAt);
}
isListening[dependency] = true;
}
}
},
对于同一个事件,例如click
有两个事件 onClick
(在冒泡阶段触发) onClickCapture
(在捕获阶段触发)两个事件名,这个冒泡和捕获都是react
事件模拟出来的。绑定到 document
上面的事件基本上都是在冒泡阶段(对 whell, focus, scroll 有额外处理),如下图 click
事件绑定执行的如下。
topEventMapping
是 topLevlelEvent
浏览器事件对照关系,mountAt
是绑定对象是函数接收第二个参数,也就是上文的doc
(document)。
ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent
对所传的target
做了非空判断后调用 EventListener.listen
传参数分别是:事件对象, 浏览器原生事件名称, 指定了顶级事件类型的事件处理函数(bind函数)ReactEventListener.dispatchEvent.bind(null, topLevelType)
。
EventListener.listen
将事件绑定到target
上。
回到上文利用事务存储事件部分,这里调用的putListener方法
调用 EventPluginHub.putListener
第一个参数是组件事例,第二个是‘onClick’,第三个是我们写的事件处理函数
putListener
将事件处理函数存储到listenerBank[registrationName][key]
上其中registrationName
是事件名称,.${_rootNodeID}``作为key值,处理函数作为
value存储。下面调用的方法有对与
safraiclick`事件的兼容处理。
至此事件绑定告一段落了。
2 事件处理
event pooling事件池
合成事件是 pooled(循环使用的),这意味着合成事件对象会被重复使用,所有的属性在被调用以后会被值为null,该机制用于性能优化,因此你不可以异步访问事件。除非调用event.persist()
,该方法不会不会把事件放入事件池中,保持event对象不被重置允许代码的引用到。
事件触发后执行dispatchEvent
方法,该方法第一个参数是绑定时bind的 topLevelEvent
这里是 topClick
,此处调用TopLevelCallbackBookKeeping.getPooled
函数先去事件池中取可以复用的,没有的话初始化新的。
这个bookKeeping初始化很简单,就是把顶级事件类型,原生事件对象,空的父组件列表放在一个对象上。
reactUpdate.batchedUpdates
是用事务封装了handleTopLevelImpl(bookKeeping)
。
getEventTarget
返回的是对应的Dom节点
ReactDOMComponentTree.getClosestInstanceFromNode
返回对应的 reactDomComponent
执行事件回调前,先由当前组件向上遍历它的所有父组件。保存到bookKeeping.ancestors
这个数组中。因为事件回调中可能会改变DOM结构,所以要先遍历好组件层级,防止与已缓存ReactMount's node
相矛盾。之后就是依次掉调用 ReactEventListener._handleTopLevel
最后一个参数通过getEventTarget函数兼容svg
以及safrai
的 textNode
这里最终返回的是触发事件的DOM节点。
handleTopLevel
函数经由EventPluginHub
处理 top level Event
,在EventPluginHub处理过程中不同的plugin可以创建派发相应的事件。第一行是构造出合成事件,第二行就是交由事务处理事件。
2.1 构建react事件
extractEvent
让已注册的plugin
处理相应的的topLevelType
。下图看到在运行过程中已注册的plugin只有五个分别是
ReactInjection.EventPluginHub.injectEventPluginsByName({
SimpleEventPlugin: SimpleEventPlugin,
EnterLeaveEventPlugin: EnterLeaveEventPlugin,
ChangeEventPlugin: ChangeEventPlugin,
SelectEventPlugin: SelectEventPlugin,
BeforeInputEventPlugin: BeforeInputEventPlugin
});
extractEvent
会依次调用每个plugin
的extractEvents
方法,第一个处理的是SimpleEventPlugin
,该plugin
处理了绝大部分的事件,本例 onClick
就是其中之一。
经由一个switch(topLevelType)
确定该react事件的构造函数为SyntheticMouseEvent
上文看到 topClick
使用 syntheticMouseEvent
作为事件构造函数。
这里调用的EventConstructor.getPooled
就是开篇提到的事件池,先看有没有可以复用的事件对象没有的话在重新实例一个。
这里SyntheticMouseEvent
调用 SyntheticUIEvent
, SyntheticUIEvent
调用 SyntheticEvent
。SyntheticEvent
构造函数这部分代码相对较长,函数注释中说道,该方法应该尽量减少调用的频率,使用pooling
(回收再利用|池)机制。在构建时候会通过判断isPersistent
属性来判断调用后是否放入池中。使用者可以通过调用 persist
方法来改变这个值。
而后执行的是 EventPropagators.accumulateTwoPhaseDispatches(event)
这个方法经历层层跳转,详情可见调用栈,最后到traverseTwoPhase
这个函数。inst
为 触发事件的reactDomComponent
,fn
为 accumulateDirectionDispatches
, arg
为合成事件。
function traverseTwoPhase(inst, fn, arg) {
var path = [];
while (inst) {
path.push(inst);
inst = inst._hostParent;
}
var i;
for (i = path.length; i-- > 0;) {
fn(path[i], 'captured', arg);
}
for (i = 0; i < path.length; i++) {
fn(path[i], 'bubbled', arg);
}
}
path
为收集的以target起始到根节点为止的组件,本例中两个。用于后续模拟事件的捕获和冒泡。
之后按照从外到内捕获从里到外冒泡的顺序调用 accumulateDirectionDispatches(path[i], 'captured', arg)
该方法将合成事件与处理函数联系起来。
这里 listenerAtPhase
-> getListener[EventPluginHub.js] 获取事件处理函数。
在事件绑定中最后把所有的事件处理放在一个对象上listenerBank
。
通过注册类型获取到对应类型的所有处理函数,使用.${reactDomComponent._rootNodeID}
找到对应虚拟Dom上的事件处理函数。
/**
* @param {object} inst reactDOMComponent 实例 (虚拟DOM)
* @param {string} registrationName 注册事件名
* * /
getListener: function (inst, registrationName) {
// TODO: shouldPreventMouseEvent is DOM-specific and definitely should not
// live here; needs to be moved to a better place soon
// 获取同类型的所有处理函数
var bankForRegistrationName = listenerBank[registrationName];
if (shouldPreventMouseEvent(registrationName, inst._currentElement.type, inst._currentElement.props)) {
return null;
}
// 获取 .${reactDomComponent._rootNodeID}`
var key = getDictionaryKey(inst);
// 返回指定虚拟DOM上的事件处理函数
return bankForRegistrationName && bankForRegistrationName[key];
}
获取事件处理函数后,将它和响应的reactDOMComponent
分别添加到队列中。accumulateInto
用于将内容添加到现有队列中,传入原来队列和要添加到队列中的内容。
至此事件已经封装准备好了。
2.2 事件分发
承接上文封装好的event
对象。使用runEventQueueInBatch
开始事件分发。
这里第一行用于将事件放入队列processEventQueue
中,其内部调用的还是accumulateInto
方法。
第二行,processEventQueue
派发所有在事件队列processEventQueue
中的合成事件。
首先将队列中的内容取出,清空队列,以防处理中队列变化。
simulated:为true表示React测试代码,我们一般都是false
此注解出自参考文章一
这里forEachAccumulate
就是对第一个参数执行foreach
调用第二个参数。
executeDispatchesAndReleaseTopLevel
-> executeDispatchesAndRelease
该函数 -> EventPluginUtils.executeDispatchesInOrder
,并将没有调用persist的事件对象回收到事件池。
处理函数是多个,则依次执行。本例中只有一个处理函数 -> executeDispatch
。执行后设置 event._dispatchListener
和 event._dispatchInstances
为 null。
通过EventPluginUtils.getNodeFromInstance
获取响应的对应的真实DOM节点作为事件的currentTarget
。
本例执行85行 这里的type 为click,func为事件处理函数, event为合成事件对象。
在生产环境中,会直接调用事件处理函数,开发环境中会模拟浏览器事件。
模拟过程如下。
这里在创建的fakeElement上绑定事件,之后模拟事件触发(执行本例中的事件处理函数),再注销事件绑定。
到此为止这个事件已经处理完,接下来就是把这个事件属性置为null,然后把它放入事件池中了。判断是否强制了调用了persistent,没有的话就释放事件对象。
其实这里可以看到事件池有一个上线就是10,当可用的对象大于10也不会再往里面添加了。
最后看一下事件的 destructor
方法
这里获取所有的属性设置为null,并且再访问该事件对象时会预警提醒。
至此事件处理完成。
3 事件机制总结
这里是源码注释的翻译
- 顶级代理是用于捕获多数原生浏览器事件,这些只会在主线程发生,并由
reactEventLister
负责处理,reactEventLister 是被注入的因此可以支持插件事件资源,这是唯一在主线程执行的。 - 封装了顶层事件(TopLevelEvent)来应对浏览器异常。这个在工作线程完成。
- 传递原生事件以及封装的顶层事件名称到
EventPluginHub
,他会遍历插件是否要执行某些合成事件。 -
EventPluginHub
获取响应的事件监听器,以及Dom绑定到生成的事件对象上。 -
EventPluginHub
将会派发事件
3.1 各种事件名
主要三个事件:regiestrationName(注册事件名),topLevelType(顶层事件),(原生事件)
事件绑定阶段,从组件属性中获取’注册事件名‘,会区分捕获和默认冒泡事件名,这里的注册名为react对外暴露的事件,包含自定义事件。
顶层事件是react
封装EventPlugin
处理的单位,react对外暴露的事件是由一个多个事件模拟而成的。
原生事件是最终绑定到目标元素上的事件,和顶层事件对应关系为一对一的关系。在绑定给document
的是使用bind函数,固定第一个参数——topLevelEvent的函数。因此当事件触发后使用的。
// 本例中
// regiestrationName(注册名)
onClick
onClickCapture
// topLevelType (顶层事件类型)
topClick
// native Event (原生事件) | dependence
click
// regiestrationName => topLevelType
EventPluginRegisterName.registionNameDependencies
// topLevelType => native event
topEventMapping[位于reactBrowserEventEmitter.js]
3.2 事件全局代理(target)
根据不同的topLevelType
对应的浏览器事件,绑定到target
上(如果是fragement
就是根节点(在reactDom.render指定的),不是的话就是document
)ReactEventListener.dispatchEvent.bind(null, topLevelType)。
3.3 事件存储
当组件渲染和更新的时候会调用_updateDomPorperties
方法检查属性变化,这里执行reactBrowerEventEmitter
模块下的listenTo
对不同事件进行了兼容处理后最终调用 EventPluginHub.js
模块下的 putListener
方法,将事件处理函数,以 .${reactDomComponent._rootNodeID}
为key值放在listenerBank[registrationName]
对象上。
3.4 阻止事件冒泡
通过事件绑定的分析会发现,无论注册的是onClick
还是 onClickCapture
最后都是调用 ReactBrowserEventEmitter.ReactEventListener.trapBubbledEvent
在冒泡阶段触发的事件, 也会发现在没有执行事件处理函数的时候,事件就已经eventQuene
中,那是不是就意味着调用e.stopPropagation()
就不能阻止事件冒泡了呢。
事件处理函数中获取的事件是合成事件对象,合成事件对象也是有stopPropagation
方法的。
注意这里的最后一行,这里执行的函数为事件isPropagationStopped
方法赋值了一个只会返回true
的函数。而在一次调用事件处理函数的过程中,每一次都会调用事件对象的该方法。
因此使用e.stopPropagation()
不能组织原生事件冒泡,但是模拟到阻止事件冒泡的效果的。
react 文档说明
更多可参考[4]
3.5 事件相关文件
synthetcEvent 封装合成事件基类
原型方法:
- preventDefault()
- stopPropergation()
- persist() 调用后
isPersist = true
, 此事件对象将不会被销毁复用(进入事件池) - isPersist
- desturctor() 事件触发后(isPersist!==true), 清空事件对象属性。
**静态方法: **
- arugumentClass
// @prarm interface 需要定义的事件对象属性
// @param Class 子类
SyntheticEvent.augmentClass = function(Class, Interface) {
var Super = this;
var E = function() {};
E.prototype = Super.prototype;
var prototype = new E();
Object.assign(prototype, Class.prototype);
// 子类继承基类原型上的方法
Class.prototype = prototype;
Class.prototype.constructor = Class;
// 合并interface
Class.Interface = Object.assign({}, Super.Interface, Interface);
Class.augmentClass = Super.augmentClass;
// 为子类添加事件池相关属性和方法
addEventPoolingTo(Class);
};
- eventPool[]
- getPooled()
参数同构造函数传参,判断事件池中是否有可用事件,有的复用,没有新建。 - release(event)
判断事件对象是否isPersist
过 没有的话调用对象的destructor
, 之后将其添加入事件池。
参考
React源码分析7 — React合成事件系统
看源码react事件机制
React源码解读系列 – 事件机制
react 合成事件和原生事件的阻止冒泡