大家好,我卡颂。
因为如下起因,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 buttonclick 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
之间所有注册的该事件对应回调 - 反向遍历并执行一遍所有收集的回调(模仿捕捉阶段的实现)
- 正向遍历并执行一遍所有收集的回调(模仿冒泡阶段的实现)
首先,实现第一步:
// 步骤1const 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个步骤实现。
总的来说,就是这么简略。