大家好,我卡颂。
由于如下原因,React
的事件系统代码量很大:
- 需要抹平不同浏览器的差异
- 与内部的优先级机制绑定
- 需要考虑所有浏览器事件
但如果抽丝剥茧会发现,事件系统的核心只有两个模块:
- SyntheticEvent(合成事件)
- 模拟实现的事件传播机制
本文会用60行代码实现这两个模块,让你快速了解React
事件系统的原理。
欢迎加入人类高质量前端框架群,带飞
Demo的效果
对于如下这段JSX
:
1 | const jsx = ( |
在浏览器中渲染:
1 | const root = document.querySelector("#root"); |
点击按钮,会依次打印:
1 | click button |
如果在button
的点击回调中增加e.stopPropagation()
,点击后会打印:
click button
我们的目标是将JSX
中的onClick
替换为ONCLICK
,但是点击后的效果不变。
也就是说,我们将基于React
自制一套事件系统,他的事件名的书写规则是形如ONXXX的全大写
形式。
实现SyntheticEvent
首先,我们来实现SyntheticEvent
(合成事件)。
SyntheticEvent
是浏览器原生事件对象的一层封装。兼容所有浏览器,同时拥有和浏览器原生事件相同的API,如stopPropagation()
和preventDefault()
。
SyntheticEvent
存在的目的是抹平浏览器间在事件对象
间的差异,但是对于不支持某一事件的浏览器,SyntheticEvent
并不会提供polyfill
(因为这会显著增大ReactDOM
的体积)。
我们的实现很简单:
1 | class SyntheticEvent { |
接收原生事件对象,返回一个包装对象。原生事件对象
会保存在nativeEvent
属性中。
同时,实现了stopPropagation
方法。
实际的SyntheticEvent会包含更多属性和方法,这里为了演示目的简化了
实现事件传播机制
事件传播机制的实现步骤如下:
- 在根节点绑定
事件类型
对应的事件回调,所有子孙节点触发该类事件最终都会委托给根节点的事件回调处理。 - 寻找触发事件的DOM节点,找到其对应的
FiberNode
(即虚拟DOM节点) - 收集从当前
FiberNode
到根FiberNode
之间所有注册的该事件对应回调 - 反向遍历并执行一遍所有收集的回调(模拟捕获阶段的实现)
- 正向遍历并执行一遍所有收集的回调(模拟冒泡阶段的实现)
首先,实现第一步:
1 | // 步骤1 |
在入口处注册点击回调
:
1 | const root = document.querySelector("#root"); |
接下来实现根节点的事件回调:
1 | const dispatchEvent = (e, type) => { |
接下来收集路径中该事件的所有回调函数。
收集路径中的事件回调函数
实现的思路是:从当前FiberNode
一直向上遍历,直到根FiberNode
。收集遍历过程中的FiberNode.memoizedProps
属性内保存的对应事件回调:
1 | const collectPaths = (type, begin) => { |
得到的paths
结构类似如下:
捕获阶段的实现
由于我们是从目标FiberNode
向上遍历,所以收集到的回调的顺序是:
1 | [目标事件回调, 某个祖先事件回调, 某个更久远的祖先回调 ...] |
要模拟捕获阶段
的实现,需要从后向前遍历数组并执行回调。
遍历的方法如下:
1 | const triggerEventFlow = (paths, type, se) => { |
注意,我们在SyntheticEvent
中实现的stopPropagation
方法,调用后会阻止遍历的继续。
冒泡阶段的实现
有了捕获阶段
的实现经验,冒泡阶段很容易实现,只需将paths
反向后再遍历一遍就行。
总结
React
事件系统的核心包括两部分:
- SyntheticEvent
- 事件传播机制
事件传播机制
由5个步骤实现。
总的来说,就是这么简单。