网站首页 > 技术文章 正文
作者:卡颂
由于如下原因,React的事件系统代码量很大:
- 需要抹平不同浏览器的差异
- 与内部的「优先级机制」绑定
- 需要考虑所有浏览器事件
但如果抽丝剥茧会发现,事件系统的核心只有两个模块:
- SyntheticEvent(合成事件)
- 模拟实现的事件传播机制
本文会用60行代码实现这两个模块,让你快速了解React事件系统的原理。
在线DEMO地址[1]
Demo的效果
对于如下这段JSX:
复制const jsx = (
<section onClick={(e) => console.log("click section")}>
<h3>你好</h3>
<button
onClick={(e) => {
// e.stopPropagation();
console.log("click button");
}}
>
点击
</button>
</section>
);1.2.3.4.5.6.7.8.9.10.11.12.13.
在浏览器中渲染:
复制const root = document.querySelector("#root");
ReactDOM.render(jsx, root);1.2.
点击按钮,会依次打印:
复制click button
click section1.2.
如果在button的点击回调中增加e.stopPropagation(),点击后会打印:
复制click button1.
我们的目标是将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();
}
}
}1.2.3.4.5.6.7.8.9.10.11.
接收「原生事件对象」,返回一个包装对象。原生事件对象会保存在nativeEvent属性中。
同时,实现了stopPropagation方法。
实际的SyntheticEvent会包含更多属性和方法,这里为了演示目的简化了
实现事件传播机制
事件传播机制的实现步骤如下:
- 在根节点绑定事件类型对应的事件回调,所有子孙节点触发该类事件最终都会委托给「根节点的事件回调」处理。
- 寻找触发事件的DOM节点,找到其对应的FiberNode(即虚拟DOM节点)
- 收集从当前FiberNode到根FiberNode之间所有注册的「该事件对应回调」
- 反向遍历并执行一遍所有收集的回调(模拟捕获阶段的实现)
- 正向遍历并执行一遍所有收集的回调(模拟冒泡阶段的实现)
首先,实现第一步:
复制// 步骤1
const addEvent = (container, type) => {
container.addEventListener(type, (e) => {
// dispatchEvent是需要实现的“根节点的事件回调”
dispatchEvent(e, type.toUpperCase(), container);
});
};1.2.3.4.5.6.7.
在入口处注册点击回调:
复制const root = document.querySelector("#root");
ReactDOM.render(jsx, root);
// 增加如下代码
addEvent(root, "click");1.2.3.4.
接下来实现「根节点的事件回调」:
复制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);
}
};1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.24.
接下来收集路径中「该事件的所有回调函数」。
收集路径中的事件回调函数
实现的思路是:从当前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;
};1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.17.18.19.20.21.22.23.
得到的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;
}
}
};1.2.3.4.5.6.7.8.9.10.11.12.13.14.15.16.
注意,我们在SyntheticEvent中实现的stopPropagation方法,调用后会阻止遍历的继续。
冒泡阶段的实现
有了捕获阶段的实现经验,冒泡阶段很容易实现,只需将paths反向后再遍历一遍就行。
总结React事件系统的核心包括两部分:
- SyntheticEvent
- 事件传播机制
事件传播机制由5个步骤实现。
总的来说,就是这么简单。
参考资料
[1]在线DEMO地址:
https://codesandbox.io/s/optimistic-torvalds-9ufc5?file=/src/index.js
来源: 魔术师卡颂
猜你喜欢
- 2024-09-20 Vue.js常见的20道前端面试题及答案
- 2024-09-20 AI面试官:我能胜任React(二)(我能胜任这份工作吗)
- 2024-09-20 70个JavaScript知识点详细总结(上)【实践】
- 2024-09-20 ReactDOM.render和ReactDOM.createPortal的区别
- 2024-09-20 学会使用Vue JSX,一车老干妈都是你的
- 2024-09-20 字节P8大佬爆肝整理,一文带你梳理React面试题!
- 2024-09-20 2024前端面试真题之—VUE篇(2020web前端经典面试题 vue)
- 2024-09-20 JS事件绑定的常用方式实例总结(js事件绑定的四种方式)
- 2024-09-20 JavaScript 的基本术语大全(javascript的基本概念)
- 2024-09-20 React18内核探秘:手写React高质量源码迈向高阶开发(超清完结)
- 1514℃桌面软件开发新体验!用 Blazor Hybrid 打造简洁高效的视频处理工具
- 563℃Dify工具使用全场景:dify-sandbox沙盒的原理(源码篇·第2期)
- 508℃MySQL service启动脚本浅析(r12笔记第59天)
- 486℃服务器异常重启,导致mysql启动失败,问题解决过程记录
- 485℃启用MySQL查询缓存(mysql8.0查询缓存)
- 465℃「赵强老师」MySQL的闪回(赵强iso是哪个大学毕业的)
- 445℃mysql服务怎么启动和关闭?(mysql服务怎么启动和关闭)
- 442℃MySQL server PID file could not be found!失败
- 最近发表
- 标签列表
-
- c++中::是什么意思 (83)
- 标签用于 (65)
- 主键只能有一个吗 (66)
- c#console.writeline不显示 (75)
- pythoncase语句 (81)
- es6includes (73)
- windowsscripthost (67)
- apt-getinstall-y (86)
- node_modules怎么生成 (76)
- c++int转char (75)
- static函数和普通函数 (76)
- el-date-picker开始日期早于结束日期 (70)
- js判断是否是json字符串 (67)
- checkout-b (67)
- c语言min函数头文件 (68)
- asynccallback (71)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- & (66)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- eacces (67)
- 查看mysql是否启动 (70)
- 无效的列索引 (74)