1.DOM事件
- 事件流包含三个阶段
- 事件捕获阶段
- 处于目标阶段
- 事件冒泡阶段
- DOM事件首发发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是冒泡阶段
2.事件捕获阶段
- 是先由最上一级的节点先接收事件,然后向下传播到具体的节点
document>body>div>button
3.目标阶段
- 事件真正触发事件的对象
4.事件冒泡
- 事件到了具体接收阶段的事件对象一步一步逐级向上传播
button>div>body>document
5.addEventListener
- 在任何发生在W3C事件模型中的事件,都是
捕获>目标>冒泡 - 可以选择在捕获阶段或者在冒泡阶段进行事件绑定
useCapture参数是ture是在捕获阶段绑定函数,否则就是在冒泡阶段绑定函数
element.addEventListener(event, function, useCapture)
5.1 阻止冒泡
- 在微软的模型中设置事件的
cancelBubble的属性为true - 在W3C中调用
stopPropagtion()方法
function stopPropagation(event) {
if (!event) { window.event.cancelBubble = true; }
if (event.stopPropagation) { event.stopPropagation();
}
}
5.2 阻止默认行为
- 在微软的模型中设置事件的
returnValue的属性为false - 在W3C中调用
preentDefault()方法
function preventDefault(event) {
if (!event) { window.event.returnValue = false; }
if (event.preventDefault) { event.preventDefault();
}
}
5.3 事件代理
- 事件代理又称
事件委托 - 事件代理就是把原本给子节点要绑定的元素 委托给父子节点监听
- 事件代理是利用
事件冒泡来实现的 - 事件代理优点
- 可以大量节省内存占用,减少事件注册
- 当新增子对象时无需再次对其绑定
<body>
<ul id="list" onclick="show(event)">
<li>item 1</li>
<li>item 2</li>
<li>item 3</li>
<li>item n</li>
</ul>
<script> function show(event) {
alert(event.target.innerHTML);
}
</script>
</body>
6.React 事件系统
- React 合成事件是围绕浏览器原生事件充当跨浏览器包装器的对象,它将不同浏览器行为合成一个统一的API 对外提供属性或者方法,这样可以磨平不同浏览器或者不同平台的直接的差异
优势
- 跨浏览器兼容性:React 的合成事件系统解决了不同浏览器之间事件处理的差异,使得开发者不需要担心不同浏览器对事件的支持情况,保证了事件行为的一致性。
- 性能优化:React 的合成事件系统采用了事件委派(event delegation)和事件池(event pooling)等技术来优化性能。事件委派可以减少内存占用,并且可以在整个组件树中统一管理事件处理,而事件池可以重用事件对象,减少对象创建和垃圾回收的开销。
- 方便的事件处理:合成事件系统提供了一种更方便的方式来处理事件,可以直接在 JSX 中声明事件处理函数,而不需要手动添加事件监听器或者操作 DOM 元素。
- 自动绑定上下文:合成事件会自动绑定事件处理函数的上下文(this),这样你不需要担心函数内部的 this 指向问题,因为 React 已经帮你处理好了。
- 性能优化的空间:由于合成事件系统的存在,React 可以在事件处理过程中进行一些性能优化,比如批量更新、事件冒泡的控制等等。
React 事件合成系统的源码简单实现
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React</title>
</head>
<body>
<div id="root">
<button id="btn">1</button>
<div id="parent">
parent
<span id="child">child</span>
</div>
</div>
<!-- <script type="module" src="/src/main.jsx"></script> -->
<script>
let root = document.getElementById('root');
var parent = document.getElementById("parent")
var child = document.getElementById("child")
function dispatchEvent(event, isCapture) {
let paths = []
let currentTarget = event.target
while (currentTarget) {
paths.push(currentTarget)
currentTarget = currentTarget.parentNode
}
if (isCapture) {
for (let i = paths.length - 1; i >= 0; i--) {
let handler = paths[i].onClickCapture
handler && handler()
}
} else {
for (let i = 0; i < paths.length; i++) {
let handler = paths[i].onClick
handler && handler()
}
}
}
//root的捕获阶段的处理函数
root.addEventListener('click', event => dispatchEvent(event, true), true);
//root的冒泡阶段的处理函数
root.addEventListener('click', event => dispatchEvent(event, false), false);
root.addEventListener('click', event => console.log('根元素原生事件捕获'), true);
root.addEventListener('click', event => console.log('根元素原生事件冒泡'), false);
parent.addEventListener('click', () => {
console.log('父元素原生事件捕获');
}, true);
parent.addEventListener('click', () => {
console.log('父元素原生事件冒泡');
}, false);
child.addEventListener('click', () => {
console.log('子元素原生事件捕获');
}, true);
child.addEventListener('click', () => {
console.log('子元素原生事件冒泡');
}, false);
parent.onClick = () => {
console.log('React:父元素React事件冒泡');
}
parent.onClickCapture = () => {
console.log('React:父元素React事件捕获');
}
child.onClick = () => {
console.log('React:子元素React事件冒泡');
}
child.onClickCapture = () => {
console.log('React:子元素React事件捕获');
}
</script>
</body>
</html>
7.React 事件合成系统实现逻辑步骤
- 注册对应的事件 比如 click=>onClick ,close=onClose 等等。 topLevelEventsToReactNames
- 注册对应的 click , close 等等 。 allNativeEvents
- 侦听allNativeEvents中所有支持的事件
给根节点添加事件监听器 addEventListener 一个是冒泡一个是捕获 给 #root 监听的事件是下面5-11事件要做的事- 侦听两个阶段一个是冒泡一个是捕获
- 创建具有优先级的事件侦听器
- 创建事件调度器
- 创建插件事件系统的调度事件
- 根据点击的目标fiber 获取点击的节点 根据节点提取执行的事件(提取事件分为冒泡和捕获阶段)
- 根据目标节点的信息创建出统一的合成事件(合成事件里面有preventDefault()阻止捕获,stopPropagatio()阻止冒泡)这个可以磨平浏览器的差异
- 根据不同的节点执行对应收集到的函数
具体执行方法流程和对应文件名称
项目的具体目录结构
- src/main.jsx
import * as React from './react';
import { createRoot } from 'react-dom/client';
function App() {
return (
<h1
onClick={() => console.log('onClick App')}
onClickCapture={() => console.log('onClickCapture App')}
>
hello1
<span
onClick={() => console.log('onClick span')}
onClickCapture={() => console.log('onClickCapture span')}
>
world1
</span>
</h1>
);
}
let element = <App />;
const root = createRoot(document.getElementById('root'));
root.render(element);
- src\react-dom\src\client\ReactDOMRoot.js
import {
createContainer,
updateContainer,
} from "react-reconciler/src/ReactFiberReconciler";
import { listenToAllSupportedEvents } from "react-dom-bindings/src/events/DOMPluginEventSystem";
function ReactDOMRoot(internalRoot) {
this._internalRoot = internalRoot;
}
ReactDOMRoot.prototype.render = function render(children) {
const root = this._internalRoot;
root.containerInfo.innerHTML = "";
updateContainer(children, root);
};
export function createRoot(container) {
const root = createContainer(container);
listenToAllSupportedEvents(container);
return new ReactDOMRoot(root);
}
- src\react-dom-bindings\src\events\DOMEventProperties.js
import { registerTwoPhaseEvent } from "./EventRegistry";
export const topLevelEventsToReactNames = new Map();
const simpleEventPluginEvents = ["click"];
/**
*处理具体的事件名
*
* @param {*} domEventName 原生对应的事件名 click
* @param {*} reactName react 自己对象的事件名 onClick
*/
function registerSimpleEvent(domEventName, reactName) {
topLevelEventsToReactNames.set(domEventName, reactName);
console.log(topLevelEventsToReactNames, 'topLevelEventsToReactNames')
registerTwoPhaseEvent(reactName, [domEventName]);
}
/**
*注册整理所有事件将click 注册为onClick 等等。。。
*
* @export
*/
export function registerSimpleEvents() {
for (let i = 0; i < simpleEventPluginEvents.length; i++) {
const eventName = simpleEventPluginEvents[i]; // click
const domEventName = eventName.toLowerCase(); // click
const capitalizedEvent = eventName[0].toUpperCase() + eventName.slice(1); // Click
registerSimpleEvent(domEventName, `on${capitalizedEvent}`); // click=>onClick
}
}
- src\react-dom-bindings\src\events\EventRegistry.js
export const allNativeEvents = new Set();
/**
*注册事件对应的两个阶段 冒泡 和 捕获
*
* @export
* @param {*} registrationName 注册事件名
* @param {*} dependencies 对应的原生 事件名称
*/
export function registerTwoPhaseEvent(registrationName, dependencies) {
registerDirectEvent(registrationName, dependencies);
registerDirectEvent(registrationName + "Capture", dependencies);
}
/**
*注册具体的冒泡还是 捕获阶段 并保存在下来
*
* @export
* @param {*} registrationName
* @param {*} dependencies
*/
export function registerDirectEvent(registrationName, dependencies) {
for (let i = 0; i < dependencies.length; i++) {
allNativeEvents.add(dependencies[i]); // click
}
console.log(allNativeEvents, 'allNativeEvents')
}
- src\react-dom-bindings\src\events\DOMPluginEventSystem.js
import { allNativeEvents } from "./EventRegistry";
import * as SimpleEventPlugin from "./plugins/SimpleEventPlugin";
import { createEventListenerWrapperWithPriority } from "./ReactDOMEventListener";
import { IS_CAPTURE_PHASE } from "./EventSystemFlags";
import { addEventCaptureListener, addEventBubbleListener } from "./EventListener";
import getEventTarget from "./getEventTarget";
import getListener from "./getListener";
import { HostComponent } from "react-reconciler/src/ReactWorkTags";
SimpleEventPlugin.registerEvents();
const listeningMarker = "_reactListening" + Math.random().toString(36).slice(2);
export function listenToAllSupportedEvents(rootContainerElement) {
if (!rootContainerElement[listeningMarker]) {
rootContainerElement[listeningMarker] = true;
allNativeEvents.forEach((domEventName) => {
listenToNativeEvent(domEventName, true, rootContainerElement);
listenToNativeEvent(domEventName, false, rootContainerElement);
});
}
}
export function listenToNativeEvent(domEventName, isCapturePhaseListener, target) {
let eventSystemFlags = 0; // 冒泡 = 0 捕获 = 4
if (isCapturePhaseListener) {
eventSystemFlags |= IS_CAPTURE_PHASE;
}
addTrappedEventListener(target, domEventName, eventSystemFlags, isCapturePhaseListener);
}
function addTrappedEventListener(targetContainer, domEventName, eventSystemFlags, isCapturePhaseListener) {
const listener = createEventListenerWrapperWithPriority(targetContainer, domEventName, eventSystemFlags);
if (isCapturePhaseListener) {
addEventCaptureListener(targetContainer, domEventName, listener);
} else {
addEventBubbleListener(targetContainer, domEventName, listener);
}
}
export function dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
) {
dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer);
}
export function processDispatchQueue(dispatchQueue, eventSystemFlags) {
const inCapturePhase = (eventSystemFlags & IS_CAPTURE_PHASE) !== 0;
for (let i = 0; i < dispatchQueue.length; i++) {
const { event, listeners } = dispatchQueue[i];
processDispatchQueueItemsInOrder(event, listeners, inCapturePhase); // event system doesn't use pooling.
}
}
function processDispatchQueueItemsInOrder(event, dispatchListeners, inCapturePhase) {
if (inCapturePhase) {
for (let i = dispatchListeners.length - 1; i >= 0; i--) {
const { currentTarget, listener } = dispatchListeners[i];
if (event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
}
} else {
for (let i = 0; i < dispatchListeners.length; i++) {
const { currentTarget, listener } = dispatchListeners[i];
if (event.isPropagationStopped()) {
return;
}
executeDispatch(event, listener, currentTarget);
}
}
}
function executeDispatch(event, listener, currentTarget) {
event.currentTarget = currentTarget;
listener(event);
event.currentTarget = null;
}
function dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, targetInst, targetContainer) {
const nativeEventTarget = getEventTarget(nativeEvent);
const dispatchQueue = [];
extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
processDispatchQueue(dispatchQueue, eventSystemFlags);
}
function extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
) {
SimpleEventPlugin.extractEvents(
dispatchQueue,
domEventName,
targetInst,
nativeEvent,
nativeEventTarget,
eventSystemFlags,
targetContainer
);
}
export function accumulateSinglePhaseListeners(targetFiber, reactName, nativeEventType, inCapturePhase) {
const captureName = reactName + "Capture";
const reactEventName = inCapturePhase ? captureName : reactName;
const listeners = [];
let instance = targetFiber;
while (instance !== null) {
const { stateNode, tag } = instance;
if (tag === HostComponent && stateNode !== null) {
if (reactEventName !== null) {
const listener = getListener(instance, reactEventName);
if (listener !== null && listener !== undefined) {
listeners.push(createDispatchListener(instance, listener, stateNode));
}
}
}
instance = instance.return;
}
return listeners;
}
function createDispatchListener(instance, listener, currentTarget) {
return {
instance,
listener,
currentTarget,
};
}
- rc\react-dom-bindings\src\events\ReactDOMEventListener.js
import getEventTarget from "./getEventTarget";
import { getClosestInstanceFromNode } from "../client/ReactDOMComponentTree";
import { dispatchEventForPluginEventSystem } from "./DOMPluginEventSystem";
import {
DiscreteEventPriority, ContinuousEventPriority, DefaultEventPriority,
getCurrentUpdatePriority, setCurrentUpdatePriority
} from 'react-reconciler/src/ReactEventPriorities';
export function createEventListenerWrapperWithPriority(
targetContainer,
domEventName,
eventSystemFlags
) {
const listenerWrapper = dispatchDiscreteEvent;
return listenerWrapper.bind(null, domEventName, eventSystemFlags, targetContainer);
}
function dispatchDiscreteEvent(domEventName, eventSystemFlags, container, nativeEvent) {
const previousPriority = getCurrentUpdatePriority();
try {
setCurrentUpdatePriority(DiscreteEventPriority);
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent)
} finally {
setCurrentUpdatePriority(previousPriority);
}
}
export function dispatchEvent(domEventName, eventSystemFlags, targetContainer, nativeEvent) {
const nativeEventTarget = getEventTarget(nativeEvent);
const targetInst = getClosestInstanceFromNode(nativeEventTarget);
dispatchEventForPluginEventSystem(
domEventName,
eventSystemFlags,
nativeEvent,
targetInst,
targetContainer
);
}
export function getEventPriority(domEventName) {
switch (domEventName) {
case 'click':
return DiscreteEventPriority;
case 'drag':
return ContinuousEventPriority;
default:
return DefaultEventPriority;
}
}
需要完整的代码实现评论区call w