60行代码实现React的事件系统
大家好,我卡颂。
由于如下原因,React
的事件系统代码量很大:
- 需要抹平不同浏览器的差异
- 与内部的优先级机制绑定
- 需要考虑所有浏览器事件
- SyntheticEvent(合成事件)
- 模拟实现的事件传播机制
React
事件系统的原理。在线DEMO地址欢迎加入人类高质量前端框架群,带飞
Demo的效果 对于如下这段
JSX
:const jsx = (
console.log("click section")}>
你好
);
在浏览器中渲染:
const root = document.querySelector("#root");
ReactDOM.render(jsx, root);
点击按钮,会依次打印:
click button
click section
如果在
button
的点击回调中增加e.stopPropagation()
,点击后会打印:click button
我们的目标是将
JSX
中的onClick
替换为ONCLICK
,但是点击后的效果不变。也就是说,我们将基于
React
自制一套事件系统,他的事件名的书写规则是形如ONXXX的全大写
形式。实现SyntheticEvent 首先,我们来实现
SyntheticEvent
(合成事件)。SyntheticEvent
是浏览器原生事件对象的一层封装。兼容所有浏览器,同时拥有和浏览器原生事件相同的API,如stopPropagation()
和preventDefault()
。SyntheticEvent
存在的目的是抹平浏览器间在事件对象
间的差异,但是对于不支持某一事件的浏览器,SyntheticEvent
并不会提供polyfill
(因为这会显著增大ReactDOM
的体积)。我们的实现很简单:
class SyntheticEvent {
constructor(e) {
this.nativeEvent = e;
}
stopPropagation() {
this._stopPropagation = true;
if (this.nativeEvent.stopPropagation) {
this.nativeEvent.stopPropagation();
}
}
}
接收原生事件对象,返回一个包装对象。
原生事件对象
会保存在nativeEvent
属性中。同时,实现了
stopPropagation
方法。实际的SyntheticEvent会包含更多属性和方法,这里为了演示目的简化了实现事件传播机制 事件传播机制的实现步骤如下:
- 在根节点绑定
事件类型
对应的事件回调,所有子孙节点触发该类事件最终都会委托给根节点的事件回调处理。 - 寻找触发事件的DOM节点,找到其对应的
FiberNode
(即虚拟DOM节点) - 收集从当前
FiberNode
到根FiberNode
之间所有注册的该事件对应回调 - 反向遍历并执行一遍所有收集的回调(模拟捕获阶段的实现)
- 正向遍历并执行一遍所有收集的回调(模拟冒泡阶段的实现)
// 步骤1
const addEvent = (container, type) => {
container.addEventListener(type, (e) => {
// dispatchEvent是需要实现的“根节点的事件回调”
dispatchEvent(e, type.toUpperCase(), container);
});
};
在入口处注册
点击回调
:const root = document.querySelector("#root");
ReactDOM.render(jsx, root);
// 增加如下代码
addEvent(root, "click");
接下来实现根节点的事件回调:
const dispatchEvent = (e, type) => {
// 包装合成事件
const se = new SyntheticEvent(e);
const ele = e.target;
// 比较hack的方法,通过DOM节点找到对应的FiberNode
let fiber;
for (let prop in ele) {
if (prop.toLowerCase().includes("fiber")) {
fiber = ele[prop];
}
}// 第三步:收集路径中“该事件的所有回调函数”
const paths = collectPaths(type, fiber);
// 第四步:捕获阶段的实现
triggerEventFlow(paths, type + "CAPTURE", se);
// 第五步:冒泡阶段的实现
if (!se._stopPropagation) {
triggerEventFlow(paths.reverse(), type, se);
}
};
接下来收集路径中该事件的所有回调函数。
收集路径中的事件回调函数 实现的思路是:从当前
FiberNode
一直向上遍历,直到根FiberNode
。收集遍历过程中的FiberNode.memoizedProps
属性内保存的对应事件回调:const collectPaths = (type, begin) => {
const paths = [];
// 不是根FiberNode的话,就一直向上遍历
while (begin.tag !== 3) {
const { memoizedProps, tag } = begin;
// 5代表DOM节点对应FiberNode
if (tag === 5) {
const eventName = ("on" + type).toUpperCase();
// 如果包含对应事件回调,保存在paths中
if (memoizedProps && Object.keys(memoizedProps).includes(eventName)) {
const pathNode = {};
pathNode[type.toUpperCase()] = memoizedProps[eventName];
paths.push(pathNode);
}
}
begin = begin.return;
}return paths;
};
得到的
paths
结构类似如下:文章图片
捕获阶段的实现 由于我们是从目标
FiberNode
向上遍历,所以收集到的回调的顺序是:[目标事件回调, 某个祖先事件回调, 某个更久远的祖先回调 ...]
要模拟
捕获阶段
的实现,需要从后向前遍历数组并执行回调。遍历的方法如下:
const triggerEventFlow = (paths, type, se) => {
// 从后向前遍历
for (let i = paths.length;
i--;
) {
const pathNode = paths[i];
const callback = pathNode[type];
if (callback) {
// 存在回调函数,传入合成事件,执行
callback.call(null, se);
}
if (se._stopPropagation) {
// 如果执行了se.stopPropagation(),取消接下来的遍历
break;
}
}
};
注意,我们在
SyntheticEvent
中实现的stopPropagation
方法,调用后会阻止遍历的继续。冒泡阶段的实现 有了
捕获阶段
的实现经验,冒泡阶段很容易实现,只需将paths
反向后再遍历一遍就行。总结
React
事件系统的核心包括两部分:- SyntheticEvent
- 事件传播机制
事件传播机制
由5个步骤实现。【60行代码实现React的事件系统】总的来说,就是这么简单。
推荐阅读
- CVE-2020-16898|CVE-2020-16898 TCP/IP远程代码执行漏洞
- 关于QueryWrapper|关于QueryWrapper,实现MybatisPlus多表关联查询方式
- MybatisPlus使用queryWrapper如何实现复杂查询
- python学习之|python学习之 实现QQ自动发送消息
- 孩子不是实现父母欲望的工具——林哈夫
- opencv|opencv C++模板匹配的简单实现
- Node.js中readline模块实现终端输入
- java中如何实现重建二叉树
- 不废话,代码实践带你掌握|不废话,代码实践带你掌握 强缓存、协商缓存!
- 人脸识别|【人脸识别系列】| 实现自动化妆