We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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 实现了一个跨浏览器、高性能的事件系统,提供了一致的事件处理接口。事件系统通过事件委托的方式进行管理,即将事件监听器注册在顶层容器上,通过事件冒泡来处理不同层级的组件中的事件。
首先我们需要实现 ReactDOM 和 Reconciler 的对接,将事件的回调保存在 DOM 中,可以通过以下两个时机对接:
在 react-dom 包中新建 SyntheticEvent.ts 文件,然后新增一个 updateFiberProps 函数,将事件的回调保存在 DOM 上:
SyntheticEvent.ts
updateFiberProps
// 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 的处理:
createInstance
// 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 的处理:
commitUpdate
// 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; } };
先定义一个支持的事件类型集合,为 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 会收集经过的节点上注册的事件处理函数。
构造合成事件: 在触发事件之前,dispatchEvent 会创建一个合成事件对象 syntheticEvent,该对象会封装原生的事件对象,并添加一些额外的属性和方法。这个合成事件对象用于提供一致性的事件接口,并解决不同浏览器之间的兼容性问题。
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); } }
collectPaths 函数主要用于收集沿途的事件处理函数,并构建一个对象 paths,其中包括捕获阶段和冒泡阶段的事件处理函数列表。
collectPaths
paths
capture
bubble
targetElement
container
getEventCallbackNameFromEventType
unshift
push
// 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]; }
dispatchEvent 方法触发的事件是一个合成事件(SyntheticEvent),而不是原生事件。SyntheticEvent 对象是一个用于包装浏览器原生事件的合成事件对象,它包含了与原生事件相关的信息,可以替代浏览器的原生事件对象,具有以下特点:
SyntheticEvent
跨浏览器兼容性: SyntheticEvent 对象会在浏览器之间提供一致的事件接口,消除了一些浏览器兼容性的问题。
事件池(Event Pooling): React 使用了一个事件池,即在需要处理事件时,会从事件池中取出一个 SyntheticEvent 对象,用于包装原生事件。这个池的目的是减少垃圾回收的频率,提高性能。一旦事件处理函数执行完毕,SyntheticEvent 对象会被重置并放回池中,等待下一次使用。
事件冒泡: React 事件系统使用了事件冒泡机制,事件首先在组件的最底层触发,然后逐层向上冒泡至根节点。在冒泡的过程中,SyntheticEvent 对象会被传递给事件处理函数。由于 SyntheticEvent 是可复用的,避免了在每个事件处理中都创建新的事件对象。
提供一些额外的方法: SyntheticEvent 对象提供了一些附加的方法,例如 stopPropagation、preventDefault 等,用于阻止事件的传播和默认行为。
stopPropagation
preventDefault
属性访问: SyntheticEvent 对象的属性和方法是可访问的,与原生事件对象的属性和方法一样。例如,可以通过 event.target 获取触发事件的目标元素。
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; }
triggerEventFlow 函数主要用于遍历捕获(capture)阶段和遍历冒泡(bubble)阶段,并依次触发收集到的合成事件。
triggerEventFlow
如果事件是冒泡型事件且支持捕获阶段,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 更新流程中。
Reconciler
相关代码可在 git tag v1.11 查看,地址:https://github.com/2xiao/my-react/tree/v1.11
git tag v1.11
The text was updated successfully, but these errors were encountered:
No branches or pull requests
为了解决跨浏览器兼容性和提供一致性,使开发者可以在不同浏览器中以相同的方式处理事件,React 实现了一个跨浏览器、高性能的事件系统,提供了一致的事件处理接口。事件系统通过事件委托的方式进行管理,即将事件监听器注册在顶层容器上,通过事件冒泡来处理不同层级的组件中的事件。
1. 实现 ReactDOM 和 Reconciler 对接
首先我们需要实现 ReactDOM 和 Reconciler 的对接,将事件的回调保存在 DOM 中,可以通过以下两个时机对接:
在 react-dom 包中新建
SyntheticEvent.ts
文件,然后新增一个updateFiberProps
函数,将事件的回调保存在 DOM 上:在创建 DOM 时,可以在
createInstance
函数中增加对 props 的处理:在更新属性时,可以在
commitUpdate
函数中增加对 props 的处理:2. 模拟实现浏览器事件流程
先定义一个支持的事件类型集合,为 DOM 根节点增加事件监听:
其中,
dispatchEvent
是用于模拟浏览器事件触发过程的方法,它能够按照事件的冒泡或捕获阶段顺序触发注册的事件处理函数,并提供了一致性的事件接口,其内部处理流程大致可以分为以下几个步骤:收集沿途事件: 在事件冒泡或捕获的过程中,浏览器会按照一定的顺序触发相关的事件。在这个过程中,
dispatchEvent
会收集经过的节点上注册的事件处理函数。构造合成事件: 在触发事件之前,
dispatchEvent
会创建一个合成事件对象syntheticEvent
,该对象会封装原生的事件对象,并添加一些额外的属性和方法。这个合成事件对象用于提供一致性的事件接口,并解决不同浏览器之间的兼容性问题。遍历捕获(capture)阶段: 如果事件是冒泡型事件且支持捕获阶段,
dispatchEvent
会从根节点开始向目标节点的父级节点遍历,依次触发沿途经过的节点上注册的捕获阶段事件处理函数。遍历冒泡(bubble)阶段: 如果事件是冒泡型事件,
dispatchEvent
会从目标节点开始向根节点遍历,依次触发沿途经过的节点上注册的冒泡阶段事件处理函数。1. 收集沿途事件
collectPaths
函数主要用于收集沿途的事件处理函数,并构建一个对象paths
,其中包括捕获阶段和冒泡阶段的事件处理函数列表。paths
,包括capture
和bubble
两个数组,用于分别存储捕获阶段和冒泡阶段的事件处理函数;targetElement
开始一直循环到容器元素container
,逐级向上遍历 DOM 树。对于每个遍历到的元素,判断该元素上是否有注册的事件处理函数;getEventCallbackNameFromEventType
函数获取事件回调函数名列表,对于每个回调函数名,检查元素属性中是否存在对应的回调函数。如果存在,则将回调函数添加到paths
对象的相应阶段(捕获或冒泡)的事件处理函数数组;unshift
进capture
数组,方便后续从根节点向目标节点遍历,依次触发沿途节点上注册的捕获阶段事件处理函数;push
进bubble
数组,方便后续从目标节点向根节点遍历,依次触发沿途节点上注册的冒泡阶段事件处理函数;paths
对象,其中包含了捕获阶段和冒泡阶段的事件处理函数路径。2. 构造合成事件
dispatchEvent
方法触发的事件是一个合成事件(SyntheticEvent
),而不是原生事件。SyntheticEvent
对象是一个用于包装浏览器原生事件的合成事件对象,它包含了与原生事件相关的信息,可以替代浏览器的原生事件对象,具有以下特点:跨浏览器兼容性:
SyntheticEvent
对象会在浏览器之间提供一致的事件接口,消除了一些浏览器兼容性的问题。事件池(Event Pooling): React 使用了一个事件池,即在需要处理事件时,会从事件池中取出一个
SyntheticEvent
对象,用于包装原生事件。这个池的目的是减少垃圾回收的频率,提高性能。一旦事件处理函数执行完毕,SyntheticEvent
对象会被重置并放回池中,等待下一次使用。事件冒泡: React 事件系统使用了事件冒泡机制,事件首先在组件的最底层触发,然后逐层向上冒泡至根节点。在冒泡的过程中,
SyntheticEvent
对象会被传递给事件处理函数。由于SyntheticEvent
是可复用的,避免了在每个事件处理中都创建新的事件对象。提供一些额外的方法:
SyntheticEvent
对象提供了一些附加的方法,例如stopPropagation
、preventDefault
等,用于阻止事件的传播和默认行为。属性访问:
SyntheticEvent
对象的属性和方法是可访问的,与原生事件对象的属性和方法一样。例如,可以通过event.target
获取触发事件的目标元素。下面就来实现
SyntheticEvent
对象:3. 遍历捕获和冒泡阶段
triggerEventFlow
函数主要用于遍历捕获(capture)阶段和遍历冒泡(bubble)阶段,并依次触发收集到的合成事件。如果事件是冒泡型事件且支持捕获阶段,
dispatchEvent
会从根节点开始向目标节点的父级节点遍历,依次触发沿途经过的节点上注册的捕获阶段事件处理函数。如果事件是冒泡型事件,
dispatchEvent
会从目标节点开始向根节点遍历,依次触发沿途经过的节点上注册的冒泡阶段事件处理函数。至此,我们就实现了 React 的事件系统,解决了不同浏览器之间的事件处理差异和兼容性问题,并将事件系统对接进了
Reconciler
更新流程中。相关代码可在
git tag v1.11
查看,地址:https://github.com/2xiao/my-react/tree/v1.11The text was updated successfully, but these errors were encountered: