大家好,我卡颂。
因为如下起因,React
的事件零碎代码量很大:
- 须要抹平不同浏览器的差别
- 与外部的 优先级机制 绑定
- 须要思考所有浏览器事件
但如果抽丝剥茧会发现,事件零碎的外围只有两个模块:
- SyntheticEvent(合成事件)
- 模仿实现的事件流传机制
本文会用 60 行代码实现这两个模块,让你疾速理解 React
事件零碎的原理。
在线 DEMO 地址
欢送退出人类高质量前端框架群,带飞
Demo 的成果
对于如下这段JSX
:
const jsx = (<section onClick={(e) => console.log("click section")}>
<h3> 你好 </h3>
<button
onClick={(e) => {// e.stopPropagation();
console.log("click button");
}}
>
点击
</button>
</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 个步骤实现。
总的来说,就是这么简略。