本文由 简悦 SimpRead 转码, 原文地址 mp.weixin.qq.com
大家好,我卡颂。
由于如下原因,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>);在浏览器中渲染:
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之间所有注册的**「该事件对应回调」**反向遍历并执行一遍所有收集的回调(模拟捕获阶段的实现)
正向遍历并执行一遍所有收集的回调(模拟冒泡阶段的实现)
首先,实现第一步:
// 步骤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 个步骤实现。
总的来说,就是这么简单。
参考资料
[1]
在线 DEMO 地址: https://codesandbox.io/s/optimistic-torvalds-9ufc5?file=/src/index.js
彦祖,点个**「在看」**吧