react16.0.0 事件系统源码解析
原生事件系统
监听真实DOM,我们想监听按钮的点击事件,那么我们在按钮DOM上绑定事件和对应的回调函数。JavaScript 事件最核心的包括事件监听 (addListener)、事件触发 (emit)、事件删除 (removeListener)。
React事件系统
React是将所有的事件都绑定在网页的document,通过统一的事件监听器处理并分发,找到对应的回调函数并执行。
React为什么要自己实现一个事件系统?
1 性能
React作为一套View层面的框架,通过渲染得到vDOM(虚拟DOM),再由diff算法决定DOM树那些结点需要新增、替换或修改,假如直接在DOM结点插入原生事件监听,则会导致频繁的调用addEventListener(将指定的监听器注册到 EventTarget 上,当该对象触发指定的事件时,指定的回调函数就会被执行。)和removeEventListener(移除绑定事件),造成性能的浪费。所以React采用了事件代理的方法,对于大部分事件而言都在document上做监听,然后根据Event中的target来判断事件触发的结点。
其次React合成的SyntheticEvent(它封装了浏览器原生事件对象,并对浏览器做了兼容。他和浏览器原生事件对象有相同的接口,包括stopPropagation()和preventDefault()。)采用了池的思想,从而达到节约内存,避免频繁的创建和销毁事件对象的目的。这也是如果我们需要异步使用一个syntheticEvent,需要执行event.persist()才能防止事件对象被释放的原因。(react合成了一个SyntheticEvent方法,达到了节约内存的效果)
最后在React源码中随处可见batch做批量更新,基本上凡是可以批量处理的事情(最普遍的setState)React都会将中间过程保存起来,留到最后面flush(渲染,并最终提交到DOM树上)掉。就如浏览器对DOM树进行Style,Layout,Paint一样,都不会在操作ele.style.color='red';之后马上执行,只会将这些操作打包起来并最终在需要渲染的时候再做渲染。(react做了很多的批量操作,达到了性能优化)
2 复用
React看到在不同的浏览器和平台上,用户界面上的事件其实非常相似,例如普通的click,change等等。React希望通过封装一层事件系统,将不同平台的原生事件都封装成SyntheticEvent。(react封装的SyntheticEvent方法有很好的复用性。)
事件注册
1.判断事件名是否在react的事件名集合(registrationNameModules)中
2.在registrationNameDependencies对象中获取当前事件的依赖事件(在listenTo方法中)
3.绑定事件到document(在listen方法中)
<button onClick={this.handleClick}></button>
经由JSX解析,button会被当做组件挂载。而onClick这时候也只是一个普通的props。
precacheFiberNode方法中 设置 node[internalInstanceKey] = 一个new FiberNode()的实例
updateFiberProps方法中 设置node[internalEventHandlersKey] = props 。这里的props就是<button onClick={this.handleClick}></button /> 这里的属性。
当react遍历tree创建真实dom实例的时候,里面调用的就是createElement。而precacheFiberNode和updateFiberProps两个方法分别给domElement,真实dom.添加了两个属性(fiber-虚拟节点,props)。所有react项目中的dom都会拥有这两个属性,并且这个两个属性的属性名在同一个项目中是一致的。
这两个方法等于将真实dom和fiber,props直接关联到了一起,相互引用
fiber tree遍历
遍历当前dom节点的属性,如果其属性存在在registrationNameModules事件集合中,则执行。registrationNameModules事件集合(存储了React事件类型与浏览器原生事件类型映射的一个map),几乎包含了所有的常见事件,这也就是如果你写一些稀奇古怪的事件,react是不识别的。
ReactEventListener:负责事件注册和事件分发。React将DOM事件全都注册到document这个节点上。
topLevelType= topClick。
topLevelTypes[topLevelType]就是click。
trapBubbledEvent只是为了调用listen方法
listen就是绑定事件到document(412 将指定的监听器注册到doument上,统一回调dispatchEvent方法)
综述:listenTo就是遍历props中的event,然后将事件和事件的依赖事件统统挂载到document上,并且所有的事件的回调函数走的都是dispatchEvent。
因为所有事件都是绑定在document上的。意味着你的原生事件都执行完了之后,才能执行document的事件。dispatchEvent会做统一的派发。可以说原生事件的执行顺序是早于react事件的。(冒泡)
事件合成
由于冒泡机制,无论我们点击哪个DOM,最后都是由document响应。也即是说都会触发dispatch
在dispatch方法中我们会调用handleTopLevel方法
extractedEvents第一步是根据原生事件合成合成事件,并且在vDOM上模拟捕获冒泡,收集所有需要执行的事件回调构成回调数组。第二步是遍历回调数组,触发回调函数。
plugins是react的event模块所包含的eventPliguns插件
events = accumulateInto(events, extractedEvents);events是一个数组,accumulateInto方法等同于events.push(extractedEvents);
事件插件
var DefaultEventPluginOrder=['ResponderEventPlugin', 'SimpleEventPlugin', 'TapEventPlugin', 'EnterLeaveEventPlugin', 'ChangeEventPlugin', 'SelectEventPlugin', 'BeforeInputEventPlugin'];
其中我们最常用到的就是SimpleEventPlugin。所以这里用SimpleEventPlugin来分析。
extractEvents函数,用它生成一个合成事件,每个plugin都一定要有这个函数
forEachAccumulated就是当event不是数组的时候,直接调用accumulateTwoPhaseDispatchesSingle,参数为events。
参数
accumulateDirectionalDispatches函数将所有绑定的同类型(所有onclick事件)的回调函数存放到事件的_dispatchListeners集合里面,将回调函数对应的虚拟dom元素按顺序存放到事件的_dispatchInstances集合中,
listenerAtPhase方法 找到不同阶段(捕获/冒泡)元素绑定的回调函数
getListener用于取出我们之前存放的回调函数
事件分发
1 将事件放进队列
2 执行
enqueueEvents方法就是在eventQueue里面Push events
processEventQueue方法就是执行的过程(simulated==false),直接运行forEachAccumulated方法
forEachAccumulated在之前说过,(判断processEventQueue是否是数组),然后执行
executeDispatchesAndReleaseTopLevel方法
executeDispatchesInOrder方法通过_dispatchListeners里得到所有绑定的回调函数,再通过_dispatchInstances的绑定回调函数的虚拟dom元素。循环执行_dispatchListeners里所有的回调函数,这里有一个特殊情况,也是react阻止冒泡的原理
executeDispatch函数就是将事件对应的dom元素绑定到了currentTarget上,这样我们通过e.currentTarget就可以找到绑定事件的原生dom元素。
用这种方式初始化事件必须是由 Document.createEvent() 方法创建的实例. 本方法必须在事件被触发之前调用(用EventTarget.dispatchEvent()调用).事件 一旦被调用, 便不再做其他任何事.