跳至主要內容

11. 实现事件系统


11. 实现事件系统

摘要

  • 实现 ReactDOM 和 Reconciler 对接
  • 模拟实现浏览器事件流程

相关代码可在 git tag v1.11open in new window 查看

为了解决跨浏览器兼容性和提供一致性,使开发者可以在不同浏览器中以相同的方式处理事件,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 根节点增加事件监听:

SyntheticEvent.ts
// 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);
	});
}

其中,dispatchEvent 是用于模拟浏览器事件触发过程的方法,它能够按照事件的冒泡或捕获阶段顺序触发注册的事件处理函数,并提供了一致性的事件接口,其内部处理流程大致可以分为以下几个步骤:

  1. 收集沿途事件: 在事件冒泡或捕获的过程中,浏览器会按照一定的顺序触发相关的事件。在这个过程中,dispatchEvent 会收集经过的节点上注册的事件处理函数。

  2. 构造合成事件: 在触发事件之前,dispatchEvent 会创建一个合成事件对象 syntheticEvent,该对象会封装原生的事件对象,并添加一些额外的属性和方法。这个合成事件对象用于提供一致性的事件接口,并解决不同浏览器之间的兼容性问题。

  3. 遍历捕获(capture)阶段: 如果事件是冒泡型事件且支持捕获阶段,dispatchEvent 会从根节点开始向目标节点的父级节点遍历,依次触发沿途经过的节点上注册的捕获阶段事件处理函数。

  4. 遍历冒泡(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,包括 capturebubble 两个数组,用于分别存储捕获阶段和冒泡阶段的事件处理函数;
  • 从目标元素 targetElement 开始一直循环到容器元素 container,逐级向上遍历 DOM 树。对于每个遍历到的元素,判断该元素上是否有注册的事件处理函数;
  • 通过 getEventCallbackNameFromEventType 函数获取事件回调函数名列表,对于每个回调函数名,检查元素属性中是否存在对应的回调函数。如果存在,则将回调函数添加到 paths 对象的相应阶段(捕获或冒泡)的事件处理函数数组;
    • 其中,捕获阶段的事件要 unshiftcapture 数组,方便后续从根节点向目标节点遍历,依次触发沿途节点上注册的捕获阶段事件处理函数;
    • 冒泡阶段的事件要 pushbubble 数组,方便后续从目标节点向根节点遍历,依次触发沿途节点上注册的冒泡阶段事件处理函数;
  • 最终返回构建好的 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 对象是一个用于包装浏览器原生事件的合成事件对象,它包含了与原生事件相关的信息,可以替代浏览器的原生事件对象,具有以下特点:

  1. 跨浏览器兼容性: SyntheticEvent 对象会在浏览器之间提供一致的事件接口,消除了一些浏览器兼容性的问题。

  2. 事件池(Event Pooling): React 使用了一个事件池,即在需要处理事件时,会从事件池中取出一个 SyntheticEvent 对象,用于包装原生事件。这个池的目的是减少垃圾回收的频率,提高性能。一旦事件处理函数执行完毕,SyntheticEvent 对象会被重置并放回池中,等待下一次使用。

  3. 事件冒泡: React 事件系统使用了事件冒泡机制,事件首先在组件的最底层触发,然后逐层向上冒泡至根节点。在冒泡的过程中,SyntheticEvent 对象会被传递给事件处理函数。由于 SyntheticEvent 是可复用的,避免了在每个事件处理中都创建新的事件对象。

  4. 提供一些额外的方法: SyntheticEvent 对象提供了一些附加的方法,例如 stopPropagationpreventDefault 等,用于阻止事件的传播和默认行为。

  5. 属性访问: 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.11open in new window