60行代码实现React的事件系统
source link: https://segmentfault.com/a/1190000041355419
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
大家好,我卡颂。
由于如下原因,React
的事件系统代码量很大:
- 需要抹平不同浏览器的差异
- 与内部的优先级机制绑定
- 需要考虑所有浏览器事件
但如果抽丝剥茧会发现,事件系统的核心只有两个模块:
- SyntheticEvent(合成事件)
- 模拟实现的事件传播机制
本文会用60行代码实现这两个模块,让你快速了解React
事件系统的原理。
欢迎加入人类高质量前端框架群,带飞
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个步骤实现。
总的来说,就是这么简单。
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK