浅谈React事件系统主要分为两篇:
浅谈React事件系统-事件绑定
浅谈React事件系统-源码解读
准备工作
- 去github下载一份源码react v16.2.0
- 下载一个好用的编辑器, 支持快速定位到当前function的引用
概念
在理解React中如何实现一个事件之前, 需要声明几个概念:
- React中几乎所有的事件都是代理到document监听
- 事件是由统一的分发函数dispatchEvent进行分发
- 事件对象(event)是合成对象(SyntheticEvent), 是原生的event对象的超集
事件处理步骤
React的事件系统主要分为四部曲:
事件绑定: 在初始化组件时, 会根据当前组件传入的事件属性, 在document上绑定对应的事件及其依赖的事件
事件分发: 页面组件触发的事件根据冒泡机制, 最后会被document上的listener捕获, 之后统一由dispatchEvent进行分发, 从触发事件的节点往上递归其祖先节点, 把事件分发给它们
事件合成: 当节点拿到一个事件之后, 首先要进行根据事件的类型, 进行事件的合成, React有个基础的SyntheticEvent对象, 事件合成就是根据事件类型给这个对象添加该事件才有的属性
事件回调函数执行: 获取Fiber(虚拟dom)中的事件回调函数, 执行它
源代码
1 | 引用源码里面的一个图来描述React的事件系统 |
事件绑定:
初始化一个组件的会调用 ensureListeningTo 给document对象绑定事件.
比如一个子组件绑定了一个onChange事件,
根据事件的冒泡机制, 那么document对象就要绑定一个onChange事件,
下面的代码就是根据不同的标签类型, 给document绑定不同的事件1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27// react-dom/src/client/ReactDOMFiberComponent
export function setInitialProperties(){
switch (tag) {
case 'iframe':
...
case 'select':
ReactDOMFiberSelect.initWrapperState(domElement, rawProps);
props = ReactDOMFiberSelect.getHostProps(domElement, rawProps);
trapBubbledEvent('topInvalid', 'invalid', domElement);
ensureListeningTo(rootContainerElement, 'onChange');
break;
...
default:
props = rawProps;
}
}
// 这段代码主要就是给document绑定需要监听的事件
function ensureListeningTo(rootContainerElement, registrationName) {
var isDocumentOrFragment =
rootContainerElement.nodeType === DOCUMENT_NODE ||
rootContainerElement.nodeType === DOCUMENT_FRAGMENT_NODE;
var doc = isDocumentOrFragment
? rootContainerElement
: rootContainerElement.ownerDocument;
listenTo(registrationName, doc);
}
ensureListeningTo 最终是调用listenTo方法, listenTo主要作用是解决事件的依赖问题, 比如
onMouseEnter实际是依赖于 ‘topMouseOut’, ‘topMouseOver’两个事件, 所以document也要注册这两个事件
最终都是调用 trapCapturedEvent 或者 trapBubbledEvent 两个方法来进行实际的事件注册
1 | // react-dom/src/event/ReactBrowserEventEmitter |
1 | // react-dom/src/event/ReactDOMEventListener |
这时候, 已经完成第一个步骤: 事件绑定, 下面看一下事件是如何分发的
事件分发
1 | // react-dom/src/event/ReactDOMEventListener |
到这里, 假设有一个节点被点击了, 触发了onClick事件一直冒泡到document, document监听到这个事件, 触发 dispatchEvent方法, 遍历触发事件节点的祖先.
现在, 一个事件已经被dispatch了. 下面就是要进入到处理具体的某个事件的逻辑了
事件合成与处理 Mixin
事件合成与处理 Mixin: handleTopLevel 主要有三个逻辑
- 构造合成的事件
- 把合成事件push到 eventQueue
- processEventQueue处理eventQueue里面的事件
1 | // event/ReactEventEmitterMixin.js |
事件合成
合成事件(SyntheticEvent)
React的事件系统有很多种事件合成的插件(Plugin), 这些插件就是根据不同的事件类型, 给SyntheticEvent加各种事件特性
1 | export function extractEvents( |
主要的Plugin:
- SimpleEventPlugin
- TapEventPlugin
- Enter/LeavePlugin
先分析一下最基础的 simpleEventPlugin, 每个PluginModule都会有一个extractEvents对外的接口, 它主要的工作就是根据事件类型, 给Event对象添加一些数据特有的属性, 比如 MouseEvent就会加上 screenX, screenY, clientX, clientY 等等属性. 最终返回的是一个SyntheticEvent(合成事件对象)
1 | var SimpleEventPlugin: PluginModule<MouseEvent> = { |
上面的 accumulateTwoPhaseDispatches(event) 主要的工作:
- 获取当前虚拟dom实例绑定的事件回调函数
- 把事件回调函数push到 event._dispatchListeners 里面
- 把当前的Fiber(virtual-dom)实例push到 event._dispatchInstances 里面
这三个步骤很关键.1
2
3
4
5
6
7
8
9
10
11function accumulateDirectionalDispatches(inst, phase, event) {
// 获取当前dom节点中某某事件回调函数, 比如 onClick={(event)=>{}}
var listener = listenerAtPhase(inst, event, phase);
if (listener) {
event._dispatchListeners = accumulateInto(
event._dispatchListeners,
listener,
);
event._dispatchInstances = accumulateInto(event._dispatchInstances, inst);
}
}
enqueueEvents和processingEventQueue
上面事件已经调用各种plugin, 生成好了一个 events事件列表
接下来进行:
- 把合成的events合并到 eventQueue事件队列中(eventQueue是一个公用的事件队列)
- 遍历 processingEventQueue中的每个事件, 调用 executeDispatchesAndReleaseTopLevel去执行
1 | export function enqueueEvents(events) { |
单个事件处理单元
executeDispatchesAndReleaseTopLevel里面主要是调用 executeDispatchesAndRelease这个方法
1 | var executeDispatchesAndRelease = function( |
遍历event._dispatchListeners
下面的这个方法就是 遍历event._dispatchListeners并执行回调函数
- for循环遍历_dispatchListeners(回调函数列表)
- 如果isPropagationStopped为 true,就停止冒泡事件的执行
- 执行单个回调函数
1 | export function executeDispatchesInOrder(event, simulated) { |
执行单个回调函数
1 | function executeDispatch(event, simulated, listener, inst) { |
看到这里, 也就完整的过了一下React的事件触发到执行的过程了.
总结:
关于性能: 在看源码之前, 在网上看过各种分析React事件系统的文章, 很多人的关注点是在于事件系统的性能多么好, 确实, React通过事件代理与分发的机制, 配合Fiber架构, 在提高事件绑定效率的同时, 也能高效的完成事件的分发处理, 在代码中, 随处可见的是队列, 从事件分发开始, 就是各种push队列, 执行队列, 保证了事件系统的稳定高效运行
关于跨平台: 对于各种终端而言, 只要还没有脱离显示屏这个载体, 交互过程中的各种事件的模型其实都不会差太多. 从React的事件系统中, 一个感受就是React正在逐步的消除平台之间的差异, 各个平台都有自己事件系统, 而React作为应用层框架, 实现自己的事件系统, 通过EventPlugin的方式来丰富Event对象, 用户在React体系下开发, 不管什么平台浏览器绑定一个点击事件只需要onClick就好了, 如果说Jquery的事件消除了浏览器之间的差异, 那么React的事件系统就是消除了平台之间的差异. 层次高了那么一丢丢
回过头再看React整个体系, 从React-dom, React, React-reconciler, Event, React-CS-Render, 串起来, 也恰恰印证了React官网首页的那句slogan: A JavaScript library for building user interfaces