11. 实现事件系统
11. 实现事件系统
为了解决跨浏览器兼容性和提供一致性,使开发者可以在不同浏览器中以相同的方式处理事件,React 实现了一个跨浏览器、高性能的事件系统,提供了一致的事件处理接口。事件系统通过事件委托的方式进行管理,即将事件监听器注册在顶层容器上,通过事件冒泡来处理不同层级的组件中的事件。
1. 实现 ReactDOM 和 Reconciler 对接
首先我们需要实现 ReactDOM 和 Reconciler 的对接,将事件的回调保存在 DOM 中,可以通过以下两个时机对接:
- 创建 DOM 时
- 更新属性时
在 react-dom 包中新建 SyntheticEvent.ts
文件,然后新增一个 updateFiberProps
函数,将事件的回调保存在 DOM 上:
// packages/react-dom/src/SyntheticEvent.ts
export const elementPropsKey = '__props';
export interface DOMElement extends Element {
[elementPropsKey]: Props;
}
export function updateFiberProps(node: DOMElement, props: Props) {
node[elementPropsKey] = props;
}
在创建 DOM 时,可以在 createInstance
函数中增加对 props 的处理:
// packages/react-dom/src/hostConfig.ts
export const createInstance = (type: string, porps: any): Instance => {
const element = document.createElement(type) as unknown;
updateFiberProps(element as DOMElement, porps);
return element as DOMElement;
};
在更新属性时,可以在 commitUpdate
函数中增加对 props 的处理:
// packages/react-dom/src/hostConfig.ts
export const commitUpdate = (fiber: FiberNode) => {
switch (fiber.tag) {
case HostComponent:
return updateFiberProps(fiber.stateNode, fiber.memoizedProps);
case HostText:
const text = fiber.memoizedProps.content;
return commitTextUpdate(fiber.stateNode, text);
default:
if (__DEV__) {
console.warn('未实现的 commitUpdate 类型', fiber);
}
break;
}
};
2. 模拟实现浏览器事件流程
先定义一个支持的事件类型集合,为 DOM 根节点增加事件监听:
// packages/react-dom/src/SyntheticEvent.ts
// 支持的事件类型
const validEventTypeList = ['click'];
// 初始化事件
export function initEvent(container: Container, eventType: string) {
if (!validEventTypeList.includes(eventType)) {
console.warn('initEvent 未实现的事件类型', eventType);
return;
}
if (__DEV__) {
console.log('初始化事件', eventType);
}
container.addEventListener(eventType, (e: Event) => {
dispatchEvent(container, eventType, e);
});
}
// packages/react-dom/src/root.ts
import { initEvent } from './SyntheticEvent';
// ReactDOM.createRoot(root).render(<App />);
export function createRoot(container: Container) {
const root = createContainer(container);
return {
render(element: ReactElementType) {
initEvent(container, 'click');
return updateContainer(element, root);
}
};
}
其中,dispatchEvent
是用于模拟浏览器事件触发过程的方法,它能够按照事件的冒泡或捕获阶段顺序触发注册的事件处理函数,并提供了一致性的事件接口,其内部处理流程大致可以分为以下几个步骤:
收集沿途事件: 在事件冒泡或捕获的过程中,浏览器会按照一定的顺序触发相关的事件。在这个过程中,
dispatchEvent
会收集经过的节点上注册的事件处理函数。构造合成事件: 在触发事件之前,
dispatchEvent
会创建一个合成事件对象syntheticEvent
,该对象会封装原生的事件对象,并添加一些额外的属性和方法。这个合成事件对象用于提供一致性的事件接口,并解决不同浏览器之间的兼容性问题。遍历捕获(capture)阶段: 如果事件是冒泡型事件且支持捕获阶段,
dispatchEvent
会从根节点开始向目标节点的父级节点遍历,依次触发沿途经过的节点上注册的捕获阶段事件处理函数。遍历冒泡(bubble)阶段: 如果事件是冒泡型事件,
dispatchEvent
会从目标节点开始向根节点遍历,依次触发沿途经过的节点上注册的冒泡阶段事件处理函数。
// packages/react-dom/src/root.ts
function dispatchEvent(container: Container, eventType: string, e: Event) {
const targetElement = e.target;
if (targetElement == null) {
console.warn('事件不存在targetElement', e);
return;
}
// 收集沿途事件
const { bubble, capture } = collectPaths(
targetElement as DOMElement,
container,
eventType
);
// 构造合成事件
const syntheticEvent = createSyntheticEvent(e);
// 遍历捕获 capture 事件
triggerEventFlow(capture, syntheticEvent);
// 遍历冒泡 bubble 事件
if (!syntheticEvent.__stopPropagation) {
triggerEventFlow(bubble, syntheticEvent);
}
}
1. 收集沿途事件
collectPaths
函数主要用于收集沿途的事件处理函数,并构建一个对象 paths
,其中包括捕获阶段和冒泡阶段的事件处理函数列表。
- 在函数开始时,创建一个对象
paths
,包括capture
和bubble
两个数组,用于分别存储捕获阶段和冒泡阶段的事件处理函数; - 从目标元素
targetElement
开始一直循环到容器元素container
,逐级向上遍历 DOM 树。对于每个遍历到的元素,判断该元素上是否有注册的事件处理函数; - 通过
getEventCallbackNameFromEventType
函数获取事件回调函数名列表,对于每个回调函数名,检查元素属性中是否存在对应的回调函数。如果存在,则将回调函数添加到paths
对象的相应阶段(捕获或冒泡)的事件处理函数数组;- 其中,捕获阶段的事件要
unshift
进capture
数组,方便后续从根节点向目标节点遍历,依次触发沿途节点上注册的捕获阶段事件处理函数; - 冒泡阶段的事件要
push
进bubble
数组,方便后续从目标节点向根节点遍历,依次触发沿途节点上注册的冒泡阶段事件处理函数;
- 其中,捕获阶段的事件要
- 最终返回构建好的
paths
对象,其中包含了捕获阶段和冒泡阶段的事件处理函数路径。
// packages/react-dom/src/root.ts
type EventCallback = (e: Event) => void;
interface Paths {
capture: EventCallback[];
bubble: EventCallback[];
}
function collectPaths(
targetElement: DOMElement,
container: Container,
eventType: string
) {
const paths: Paths = {
capture: [],
bubble: []
};
// 收集
while (targetElement && targetElement !== container) {
const elementProps = targetElement[elementPropsKey];
if (elementProps) {
const callbackNameList = getEventCallbackNameFromEventType(eventType);
if (callbackNameList) {
callbackNameList.forEach((callbackName, i) => {
const callback = elementProps[callbackName];
if (callback) {
if (i == 0) {
paths.capture.unshift(callback);
} else {
paths.bubble.push(callback);
}
}
});
}
}
targetElement = targetElement.parentNode as DOMElement;
}
return paths;
}
function getEventCallbackNameFromEventType(
eventType: string
): string[] | undefined {
return {
click: ['onClickCapture', 'onClick']
}[eventType];
}
2. 构造合成事件
dispatchEvent
方法触发的事件是一个合成事件(SyntheticEvent
),而不是原生事件。SyntheticEvent
对象是一个用于包装浏览器原生事件的合成事件对象,它包含了与原生事件相关的信息,可以替代浏览器的原生事件对象,具有以下特点:
跨浏览器兼容性:
SyntheticEvent
对象会在浏览器之间提供一致的事件接口,消除了一些浏览器兼容性的问题。事件池(Event Pooling): React 使用了一个事件池,即在需要处理事件时,会从事件池中取出一个
SyntheticEvent
对象,用于包装原生事件。这个池的目的是减少垃圾回收的频率,提高性能。一旦事件处理函数执行完毕,SyntheticEvent
对象会被重置并放回池中,等待下一次使用。事件冒泡: React 事件系统使用了事件冒泡机制,事件首先在组件的最底层触发,然后逐层向上冒泡至根节点。在冒泡的过程中,
SyntheticEvent
对象会被传递给事件处理函数。由于SyntheticEvent
是可复用的,避免了在每个事件处理中都创建新的事件对象。提供一些额外的方法:
SyntheticEvent
对象提供了一些附加的方法,例如stopPropagation
、preventDefault
等,用于阻止事件的传播和默认行为。属性访问:
SyntheticEvent
对象的属性和方法是可访问的,与原生事件对象的属性和方法一样。例如,可以通过event.target
获取触发事件的目标元素。
下面就来实现 SyntheticEvent
对象:
// packages/react-dom/src/root.ts
interface SyntheticEvent extends Event {
__stopPropagation: boolean;
}
function createSyntheticEvent(e: Event) {
const syntheticEvent = e as SyntheticEvent;
syntheticEvent.__stopPropagation = false;
const originStopPropagation = e.stopPropagation;
syntheticEvent.stopPropagation = () => {
syntheticEvent.__stopPropagation = true;
if (originStopPropagation) {
originStopPropagation();
}
};
return syntheticEvent;
}
3. 遍历捕获和冒泡阶段
triggerEventFlow
函数主要用于遍历捕获(capture)阶段和遍历冒泡(bubble)阶段,并依次触发收集到的合成事件。
如果事件是冒泡型事件且支持捕获阶段,
dispatchEvent
会从根节点开始向目标节点的父级节点遍历,依次触发沿途经过的节点上注册的捕获阶段事件处理函数。如果事件是冒泡型事件,
dispatchEvent
会从目标节点开始向根节点遍历,依次触发沿途经过的节点上注册的冒泡阶段事件处理函数。
// packages/react-dom/src/root.ts
function triggerEventFlow(
paths: EventCallback[],
syntheticEvent: SyntheticEvent
) {
for (let i = 0; i < paths.length; i++) {
const callback = paths[i];
callback.call(null, syntheticEvent);
if (syntheticEvent.__stopPropagation) {
break;
}
}
}
至此,我们就实现了 React 的事件系统,解决了不同浏览器之间的事件处理差异和兼容性问题,并将事件系统对接进了 Reconciler
更新流程中。
相关代码可在 git tag v1.11
查看,地址:https://github.com/2xiao/my-react/tree/v1.11