Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

《自己动手写 React》——【11】实现事件系统 #11

Open
2xiao opened this issue Mar 25, 2024 · 0 comments
Open

《自己动手写 React》——【11】实现事件系统 #11

2xiao opened this issue Mar 25, 2024 · 0 comments

Comments

@2xiao
Copy link
Owner

2xiao commented Mar 25, 2024

为了解决跨浏览器兼容性和提供一致性,使开发者可以在不同浏览器中以相同的方式处理事件,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 是用于模拟浏览器事件触发过程的方法,它能够按照事件的冒泡或捕获阶段顺序触发注册的事件处理函数,并提供了一致性的事件接口,其内部处理流程大致可以分为以下几个步骤:

  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.11

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant