react DOM事件

53 阅读7分钟

1.DOM事件

  • 事件流包含三个阶段
    • 事件捕获阶段
    • 处于目标阶段
    • 事件冒泡阶段
  • DOM事件首发发生的是事件捕获,然后是实际的目标接收到事件,最后阶段是冒泡阶段

2.事件捕获阶段

  • 是先由最上一级的节点先接收事件,然后向下传播到具体的节点 document>body>div>button

3.目标阶段

  • 事件真正触发事件的对象

4.事件冒泡

  • 事件到了具体接收阶段的事件对象一步一步逐级向上传播 button>div>body>document

image.png

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 对外提供属性或者方法,这样可以磨平不同浏览器或者不同平台的直接的差异

image.png

优势
  1. 跨浏览器兼容性:React 的合成事件系统解决了不同浏览器之间事件处理的差异,使得开发者不需要担心不同浏览器对事件的支持情况,保证了事件行为的一致性。
  2. 性能优化:React 的合成事件系统采用了事件委派(event delegation)和事件池(event pooling)等技术来优化性能。事件委派可以减少内存占用,并且可以在整个组件树中统一管理事件处理,而事件池可以重用事件对象,减少对象创建和垃圾回收的开销。
  3. 方便的事件处理:合成事件系统提供了一种更方便的方式来处理事件,可以直接在 JSX 中声明事件处理函数,而不需要手动添加事件监听器或者操作 DOM 元素。
  4. 自动绑定上下文:合成事件会自动绑定事件处理函数的上下文(this),这样你不需要担心函数内部的 this 指向问题,因为 React 已经帮你处理好了。
  5. 性能优化的空间:由于合成事件系统的存在,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 事件合成系统实现逻辑步骤

  1. 注册对应的事件 比如 click=>onClick ,close=onClose 等等。 topLevelEventsToReactNames
  2. 注册对应的 click , close 等等 。 allNativeEvents
  3. 侦听allNativeEvents中所有支持的事件
  4. 给根节点添加事件监听器 addEventListener 一个是冒泡一个是捕获 给 #root 监听的事件是下面5-11事件要做的事
  5. 侦听两个阶段一个是冒泡一个是捕获
  6. 创建具有优先级的事件侦听器
  7. 创建事件调度器
  8. 创建插件事件系统的调度事件
  9. 根据点击的目标fiber 获取点击的节点 根据节点提取执行的事件(提取事件分为冒泡和捕获阶段)
  10. 根据目标节点的信息创建出统一的合成事件(合成事件里面有preventDefault()阻止捕获,stopPropagatio()阻止冒泡)这个可以磨平浏览器的差异
  11. 根据不同的节点执行对应收集到的函数

具体执行方法流程和对应文件名称

ReactDOM合成事件执行流程和方法drawio.drawio.png

项目的具体目录结构

image.png

  • 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