0
点赞
收藏
分享

微信扫一扫

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会包含更多属性和方法,这里为了演示目的简化了

实现事件传播机制

事件传播机制的实现步骤如下:

  1. 在根节点绑定​​事件类型​​对应的事件回调,所有子孙节点触发该类事件最终都会委托给根节点的事件回调处理。
  2. 寻找触发事件的DOM节点,找到其对应的​​FiberNode​​(即虚拟DOM节点)
  3. 收集从当前​​FiberNode​​到根​​FiberNode​​之间所有注册的该事件对应回调
  4. 反向遍历并执行一遍所有收集的回调(模拟捕获阶段的实现)
  5. 正向遍历并执行一遍所有收集的回调(模拟冒泡阶段的实现)

首先,实现第一步:

// 步骤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个步骤实现。

总的来说,就是这么简单。

举报

相关推荐

0 条评论