微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

60行代码实现React的事件系统

作者:卡颂

由于如下原因,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

来源: 魔术师卡颂

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 [email protected] 举报,一经查实,本站将立刻删除。

相关推荐