React19源码系列之合成事件机制

React 合成事件原理,包括事件注册和派发两部分。

  • 事件注册从准备事件名开始,通过插件完成注册,将事件绑定在div#root上。
  • 事件派发需收集事件函数放入队列并执行。

例如,React 合成事件原理,如在点击之前的 Mout 阶段注册和监听原生事件,点击触发时提取并执行监听处理函数,模拟捕获和冒泡阶段的流程。

listenToAllSupportedEvents

listenToAllSupportedEvents 是一个在 React 源码中用于监听并处理所有支持的事件的函数。主要功能是为 React 应用的根容器元素(rootContainerElement添加对所有支持的原生事件的监听。它会对事件监听进行去重处理,避免重复添加相同的事件监听器。同时,对于 selectionchange 事件会进行特殊处理,因为该事件不会冒泡,需要绑定到 document 对象上。

function listenToAllSupportedEvents(rootContainerElement: EventTarget) {
  // 检查 rootContainerElement 是否已经添加了事件监听器。如果没有,则将 listeningMarker 作为属性添加到 rootContainerElement 上,并将其值设为 true,表示已经添加了事件监听器。
  if (!(rootContainerElement: any)[listeningMarker]) {
    (rootContainerElement: any)[listeningMarker] = true;

    // allNativeEvents 是一个集合,包含了 React 支持的所有原生事件。
    // 遍历所有支持的原生事件
    allNativeEvents.forEach(domEventName => {
      if (domEventName !== 'selectionchange') {
        // 不在非委托事件集合中(nonDelegatedEvents非委托事件集合)
        if (!nonDelegatedEvents.has(domEventName)) {
     // 调用 listenToNativeEvent 函数添加委托的事件监听器。参数false代表冒泡
          listenToNativeEvent(domEventName, false, rootContainerElement);
        }
     // 调用 listenToNativeEvent 函数添加非委托的事件监听器。参数true代表捕获
        listenToNativeEvent(domEventName, true, rootContainerElement);
      }
    });

    // 获取 rootContainerElement 对应的 document 对象 ownerDocument。
    const ownerDocument =
      (rootContainerElement: any).nodeType === DOCUMENT_NODE
      ? rootContainerElement
      : (rootContainerElement: any).ownerDocument;
    
    if (ownerDocument !== null) {
      if (!(ownerDocument: any)[listeningMarker]) {
        (ownerDocument: any)[listeningMarker] = true;
        // 由于 selectionchange 事件不会冒泡,需要将其绑定到 document 对象上。
        // 调用 listenToNativeEvent 函数添加 selectionchange 事件的非委托监听器。
        listenToNativeEvent('selectionchange', false, ownerDocument);
      }
    }

  }
}

变量之 allNativeEvents

allNativeEvents集合用于存储 React 所支持的所有原生 DOM 事件,并且根据配置项 enableCreateEventHandleAPI 的状态,决定是否向集合中添加特定的事件。

allNativeEvents 是一个 Set 类型的集合,Set 是 JavaScript 中的一种数据结构,它类似于数组,但成员的值都是唯一的,没有重复的值。

const allNativeEvents: Set<DOMEventName> = new Set();

if (enableCreateEventHandleAPI) {
  allNativeEvents.add('beforeblur');
  allNativeEvents.add('afterblur');
}

registerDirectEvent

注册一个直接事件。在 React 的事件系统中,它会建立事件注册名(registrationName)和对应的原生 DOM 事件依赖项(dependencies)之间的映射关系,同时将这些依赖的原生 DOM 事件添加到 allNativeEvents 集合中。

函数参数含义:

  • registrationName:类型为 string,代表事件的注册名。在 React 中,开发者通常使用的是像 onClickonChange 这样的合成事件名,这些就是注册名。
  • dependencies:类型为 Array<DOMEventName>,是一个数组,包含了该注册事件所依赖的原生 DOM 事件名。例如,onClick 可能依赖于 click 这个原生 DOM 事件。
function registerDirectEvent(
  registrationName: string,
  dependencies: Array<DOMEventName>,
) {
// registrationNameDependencies 是一个对象,用于存储事件注册名和其对应的原生 DOM 事件依赖项的映射关系。通过这行代码,将 registrationName 作为键,dependencies 数组作为值,存储到 registrationNameDependencies 对象中。这样,在后续处理事件时,就可以根据注册名找到对应的原生 DOM 事件。
  registrationNameDependencies[registrationName] = dependencies;

  // 使用 for 循环遍历 dependencies 数组,将其中的每个原生 DOM 事件名添加到 allNativeEvents 集合中。由于 Set 集合的特性,相同的事件名不会重复添加。
  for (let i = 0; i < dependencies.length; i++) {
    allNativeEvents.add(dependencies[i]);
  }
}
const registrationNameDependencies: {
  [registrationName: string]: Array<DOMEventName>,
} = {};

 示例:

// 假设 registrationNameDependencies 已经定义
const registrationNameDependencies = {};
// 假设 allNativeEvents 已经定义
const allNativeEvents = new Set();

// 注册一个直接事件
registerDirectEvent('onClick', ['click']);

// 查看映射关系
console.log(registrationNameDependencies); 
// 输出: { onClick: ['click'] }

// 查看 allNativeEvents 集合
console.log(allNativeEvents); 
// 输出: Set { 'click' }

变量之 nonDelegatedEvents

事件委托是一种利用事件冒泡原理,将事件处理程序绑定到父元素上,从而处理子元素上触发的相同类型事件的技术。

不支持委托的事件如下。

const nonDelegatedEvents: Set<DOMEventName> = new Set([
  'beforetoggle',//
  'cancel',
  'close',
  'invalid',
  'load',//
  'scroll',//
  'scrollend',
  'toggle',//
  ...mediaEventTypes,
]);

const mediaEventTypes: Array<DOMEventName> = [
  'abort',//
  'canplay',
  'canplaythrough',
  'durationchange',
  'emptied',
  'encrypted',
  'ended',
  'error',
  'loadeddata',
  'loadedmetadata',
  'loadstart',
  'pause',
  'play',
  'playing',
  'progress',
  'ratechange',
  'resize',
  'seeked',
  'seeking',
  'stalled',
  'suspend',
  'timeupdate',
  'volumechange',
  'waiting',
];

listenToNativeEvent

为指定的 EventTarget 对象(如 DOM 元素)添加原生事件监听器。该函数会根据传入的参数决定是在捕获阶段还是冒泡阶段监听事件,并调用 addTrappedEventListener 函数来实际添加事件监听器。

函数参数含义:

  • domEventName:类型为 DOMEventName,代表要监听的原生 DOM 事件名称,例如 'click''mousemove' 等。
  • isCapturePhaseListener:类型为 boolean,用于指定是否在捕获阶段监听事件。如果值为 true,则在捕获阶段监听;如果值为 false,则在冒泡阶段监听。
  • target:类型为 EventTarget,表示要添加事件监听器的目标对象,通常是一个 DOM 元素。

function listenToNativeEvent(
  domEventName: DOMEventName,
  isCapturePhaseListener: boolean,// 是否捕获
  target: EventTarget,// 容器
): void {

  // 初始化事件系统标志
  let eventSystemFlags = 0;
  
  // 设置捕获阶段标志
  if (isCapturePhaseListener) {
    // IS_CAPTURE_PHASE是常量, 1 << 2 = 4 
    eventSystemFlags |= IS_CAPTURE_PHASE;
  }

  // addTrappedEventListener 函数会根据这些参数实际为目标对象添加事件监听器。
  addTrappedEventListener(
    target,
    domEventName,
    eventSystemFlags, //事件系统标志
    isCapturePhaseListener,// 是否在捕获阶段监听的标志
  );
}

addTrappedEventListener

addTrappedEventListener 函数的主要功能是为指定的目标元素添加原生事件监听器,同时支持根据事件类型和配置决定监听的阶段(捕获阶段或冒泡阶段)以及是否使用被动监听器。它会根据不同的情况对事件监听器进行封装和处理,并最终调用相应的函数来添加事件监听器,同时返回一个取消监听的函数。

 函数参数含义:

  • targetContainer:类型为 EventTarget,表示要添加事件监听器的目标元素,通常是一个 DOM 元素。
  • domEventName:类型为 DOMEventName,代表要监听的原生 DOM 事件名称,如 'click''touchstart' 等。
  • eventSystemFlags:类型为 EventSystemFlags,是一个用于存储事件系统相关标志的变量,用于表示事件的不同状态或特性。
  • isCapturePhaseListener:类型为 boolean,用于指定是否在捕获阶段监听事件。true 表示在捕获阶段监听,false 表示在冒泡阶段监听。
  • isDeferredListenerForLegacyFBSupport:可选参数,类型为 boolean,用于支持旧版的 Facebook 相关功能的延迟监听器。
function addTrappedEventListener(
  targetContainer: EventTarget,//挂载的目标元素
  domEventName: DOMEventName,//事件名称
  eventSystemFlags: EventSystemFlags,// 0
  isCapturePhaseListener: boolean,//是否捕获
  isDeferredListenerForLegacyFBSupport?: boolean,
) {
  // 创建事件监听器包装器
  //就是将浏览器事件进行分类,分为不同的优先级,每类优先级返回不同的函数
  let listener = createEventListenerWrapperWithPriority(
    targetContainer,
    domEventName,
    eventSystemFlags,
  );


  let isPassiveListener: void | boolean = undefined;
  
  // 判断是否为被动监听器
  if (passiveBrowserEventsSupported) {
    if (
      domEventName === 'touchstart' ||
      domEventName === 'touchmove' ||
      domEventName === 'wheel'
    ) {
      isPassiveListener = true;
    }
  }

  // 处理旧版支持的目标元素
  targetContainer =
    // 如果启用了旧版的 Facebook 相关支持(enableLegacyFBSupport 为 true)并且使用了延迟监听器(isDeferredListenerForLegacyFBSupport 为 true),则将目标元素替换为其所属的 document 对象。
    enableLegacyFBSupport && isDeferredListenerForLegacyFBSupport
    ? (targetContainer: any).ownerDocument
    : targetContainer;

  // 处理旧版支持的监听器
  let unsubscribeListener;
  
  
  // 根据监听阶段和被动标志添加事件监听器
  // isCapturePhaseListener为true,表示在捕获阶段监听
  if (isCapturePhaseListener) {
    // 非被动监听
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventCaptureListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener,// 被动标识为true
      );
    } else {
      unsubscribeListener = addEventCaptureListener(
        targetContainer,
        domEventName,
        listener,
      );
    }
  } else {
    // 表示在冒泡阶段监听

    // 非被动监听
    if (isPassiveListener !== undefined) {
      unsubscribeListener = addEventBubbleListenerWithPassiveFlag(
        targetContainer,
        domEventName,
        listener,
        isPassiveListener,// 被动标识为true
      );
    } else {
      unsubscribeListener = addEventBubbleListener(
        targetContainer,
        domEventName,
        listener,
      );
    }
  }
}

createEventListenerWrapperWithPriority

根据传入的事件信息(目标容器、事件名称、事件系统标志),获取该事件的优先级,并根据不同的优先级返回对应的事件调度函数。最后,将该调度函数绑定到特定的参数上并返回。

函数参数含义:

  • targetContainer:类型为 EventTarget,表示要添加事件监听器的目标元素,通常是一个 DOM 元素。
  • domEventName:类型为 DOMEventName,代表要监听的原生 DOM 事件名称等。
  • eventSystemFlags:类型为 EventSystemFlags,是一个用于存储事件系统相关标志的变量,用于表示事件的不同状态或特性。
function createEventListenerWrapperWithPriority(
  targetContainer: EventTarget,
  domEventName: DOMEventName,
  eventSystemFlags: EventSystemFlags,// 事件系统标识
): Function {
  // 获取事件的优先级
  const eventPriority = getEventPriority(domEventName);
  
  let listenerWrapper;
  switch (eventPriority) {
      // 如果事件优先级是 DiscreteEventPriority(离散事件优先级),则将 listenerWrapper 赋值为 dispatchDiscreteEvent 函数。离散事件通常是一些需要立即处理的用户交互事件,如点击、键盘输入等。
    case DiscreteEventPriority:// 离散事件
      listenerWrapper = dispatchDiscreteEvent;
      break;

      // 如果事件优先级是 ContinuousEventPriority(连续事件优先级),则将 listenerWrapper 赋值为 dispatchContinuousEvent 函数。连续事件一般是一些连续的用户交互,如滚动、拖拽等。
    case ContinuousEventPriority:// 连续事件
      listenerWrapper = dispatchContinuousEvent;
      break;

      // 如果事件优先级是 DefaultEventPriority(默认事件优先级)或者是其他未匹配的优先级,则将 listenerWrapper 赋值为 dispatchEvent 函数。
    case DefaultEventPriority:// 默认事件
    default:
      listenerWrapper = dispatchEvent;
      break;
  }
  return listenerWrapper.bind(
    null,
    domEventName,
    eventSystemFlags,
    targetContainer,
  );
}

 不同优先级的派发函数

dispatchDiscreteEvent:处理最高优先级事件(如点击、键盘输入),立即执行。

dispatchContinuousEvent:处理中等优先级事件(如滚动、拖拽),允许中断。

dispatchEvent:处理默认优先级事件(如加载、错误),按顺序执行。

具体派发函数是如何执行的,在后续的文章会做记录。

 工具函数之 getEventPriority

function getEventPriority(domEventName: DOMEventName): EventPriority {
  switch (domEventName) {
    // Used by SimpleEventPlugin:
    case 'beforetoggle':
    case 'cancel':
    case 'click':
    case 'close':
    case 'contextmenu':
    case 'copy':
    case 'cut':
    case 'auxclick':
    case 'dblclick':
    case 'dragend':
    case 'dragstart':
    case 'drop':
    case 'focusin':
    case 'focusout':
    case 'input':
    case 'invalid':
    case 'keydown':
    case 'keypress':
    case 'keyup':
    case 'mousedown':
    case 'mouseup':
    case 'paste':
    case 'pause':
    case 'play':
    case 'pointercancel':
    case 'pointerdown':
    case 'pointerup':
    case 'ratechange':
    case 'reset':
    case 'resize':
    case 'seeked':
    case 'submit':
    case 'toggle':
    case 'touchcancel':
    case 'touchend':
    case 'touchstart':
    case 'volumechange':
    // Used by polyfills: (fall through)
    case 'change':
    case 'selectionchange':
    case 'textInput':
    case 'compositionstart':
    case 'compositionend':
    case 'compositionupdate':
    // Only enableCreateEventHandleAPI: (fall through)
    case 'beforeblur':
    case 'afterblur':
    // Not used by React but could be by user code: (fall through)
    case 'beforeinput':
    case 'blur':
    case 'fullscreenchange':
    case 'focus':
    case 'hashchange':
    case 'popstate':
    case 'select':
    case 'selectstart':
      return DiscreteEventPriority;//离散事件优先级
    case 'drag':
    case 'dragenter':
    case 'dragexit':
    case 'dragleave':
    case 'dragover':
    case 'mousemove':
    case 'mouseout':
    case 'mouseover':
    case 'pointermove':
    case 'pointerout':
    case 'pointerover':
    case 'scroll':
    case 'touchmove':
    case 'wheel':
    // Not used by React but could be by user code: (fall through)
    case 'mouseenter':
    case 'mouseleave':
    case 'pointerenter':
    case 'pointerleave':
      return ContinuousEventPriority;// 连续事件优先级
    case 'message': {
      // We might be in the Scheduler callback.
      // Eventually this mechanism will be replaced by a check
      // of the current priority on the native scheduler.
      const schedulerPriority = getCurrentSchedulerPriorityLevel();
      switch (schedulerPriority) {
        case ImmediateSchedulerPriority:
          return DiscreteEventPriority;// 离散事件优先级
          
        case UserBlockingSchedulerPriority:
          return ContinuousEventPriority;// 连续事件优先级
          
        case NormalSchedulerPriority:
        case LowSchedulerPriority:
          // TODO: Handle LowSchedulerPriority, somehow. Maybe the same lane as hydration.
          return DefaultEventPriority;//默认事件优先级
          
        case IdleSchedulerPriority:
          return IdleEventPriority;// 空闲事件
          
        default:
          return DefaultEventPriority;
      }
    }
    default:
      return DefaultEventPriority;// 默认事件
  }
}

addEventCaptureListenerWithPassiveFlag 

为指定的目标元素添加一个在事件捕获阶段触发的事件监听器,并且支持设置被动监听器标志。被动监听器是一种特殊的监听器,使用它可以提高滚动等触摸事件的性能,因为它不会阻止默认行为(即不会调用 preventDefault 方法)。

函数参数含义:

  • target:类型为 EventTarget,表示要添加事件监听器的目标元素,例如 DOM 元素、document 对象或 window 对象等。
  • eventType:类型为 string,代表要监听的事件类型,如 'touchstart''touchmove''wheel' 等。
  • listener:类型为 Function,是一个回调函数,当指定的事件在目标元素上触发并处于捕获阶段时,该回调函数将被执行。
  • passive:类型为 boolean,用于指定是否使用被动监听器。如果值为 true,则表示使用被动监听器,即监听器不会阻止默认行为;如果值为 false,则不使用被动监听器。

function addEventCaptureListenerWithPassiveFlag(
  target: EventTarget,
  eventType: string,
  listener: Function,
  passive: boolean,// 父函数调用传进来的是true
): Function {
  target.addEventListener(eventType, listener, {
    capture: true,
    passive,//passive代表不会执行preventDefault
  });
  return listener;
}

addEventCaptureListener 

为指定的目标元素(target)添加一个在事件捕获阶段触发的事件监听器。

事件捕获是事件传播的第一个阶段,事件从文档根节点开始,依次向下传播到触发事件的具体元素。该函数借助原生的 addEventListener 方法达成此功能,并且返回所添加的事件监听器函数。

function addEventCaptureListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  // 第三个参数 为true 代表捕获 
  target.addEventListener(eventType, listener, true);
  return listener;
}

工具函数之 addEventBubbleListener

addEventBubbleListener 为指定的目标元素(target)添加一个在事件冒泡阶段触发的事件监听器。

事件冒泡是指事件从最具体的元素开始触发,然后依次向上传播到其父元素,直到传播到文档根节点。

 函数参数含义:

  • target:类型为 EventTarget,表示要添加事件监听器的目标元素。在 Web 开发中,EventTarget 可以是 DOM 元素(如 divbutton 等)、document 对象或 window 对象等。
  • eventType:类型为 string,代表要监听的事件类型,例如 'click''mouseover''keydown' 等。
  • listener:类型为 Function,是一个回调函数,当指定的事件在目标元素上触发并冒泡到该元素时,这个回调函数将被执行。
  • 返回值:返回类型为 Function,即返回添加的事件监听器函数本身。
function addEventBubbleListener(
  target: EventTarget,
  eventType: string,
  listener: Function,
): Function {
  target.addEventListener(eventType, listener, false);
  return listener;
}

addEventBubbleListenerWithPassiveFlag

为指定的目标元素添加一个在事件冒泡阶段触发的事件监听器,并且支持设置被动监听器标志。事件冒泡是指事件从最具体的元素开始触发,然后依次向上传播到其父元素,直到传播到文档根节点。被动监听器的作用是提高滚动等触摸事件的性能,因为它不会阻止默认行为(即不会调用 preventDefault 方法)。

function addEventBubbleListenerWithPassiveFlag(
  target: EventTarget,
  eventType: string,
  listener: Function,
  passive: boolean,
): Function {
  target.addEventListener(eventType, listener, {
    passive,
  });
  return listener;
}

全局常量 HTMLNodeType

export const ELEMENT_NODE = 1;
export const TEXT_NODE = 3;
export const COMMENT_NODE = 8;
export const DOCUMENT_NODE = 9;
export const DOCUMENT_TYPE_NODE = 10;
export const DOCUMENT_FRAGMENT_NODE = 11;

全局常量之事件优先级

// NoEventPriority 表示没有事件优先级
const NoEventPriority: EventPriority = NoLane;

// DiscreteEventPriority 表示离散事件优先级,被映射到 SyncLane。离散事件通常是一些用户交互事件,如点击、键盘输入等,这些事件需要立即处理,以保证用户操作的即时响应。SyncLane 是同步车道,在这个车道上的更新任务会立即执行。
const DiscreteEventPriority: EventPriority = SyncLane;

// ContinuousEventPriority 表示连续事件优先级,对应 InputContinuousLane。连续事件一般是一些连续的用户交互,如滚动、拖拽等。InputContinuousLane 是用于处理连续输入事件的车道,这些事件需要在一定的时间内持续处理,以保证交互的流畅性。
const ContinuousEventPriority: EventPriority = InputContinuousLane;

// DefaultEventPriority 是默认事件优先级,映射到 DefaultLane。当没有明确指定事件优先级时,会使用默认优先级。DefaultLane 是一个通用的车道,用于处理大多数普通的更新任务
const DefaultEventPriority: EventPriority = DefaultLane;

// IdleEventPriority 表示空闲事件优先级,与 IdleLane 对应。空闲事件是那些可以在浏览器空闲时执行的任务,例如一些非关键的渲染更新或数据处理。IdleLane 上的任务会在浏览器有空闲时间时才会被执行,以避免影响用户交互的性能。
const IdleEventPriority: EventPriority = IdleLane;

全局常量之事件状态

// IS_EVENT_HANDLE_NON_MANAGED_NODE:值为 1,对应的二进制表示是 00001。这个标志用于表示事件是否处理非托管节点。
export const IS_EVENT_HANDLE_NON_MANAGED_NODE = 1;

// IS_NON_DELEGATED:使用位左移运算符 << 将 1 左移 1 位,得到值 2,二进制表示为 00010。该标志表示事件是否为非委托事件。
export const IS_NON_DELEGATED = 1 << 1;

// IS_CAPTURE_PHASE:将 1 左移 2 位,值为 4,二进制表示为 00100。用于表示事件是否处于捕获阶段。
export const IS_CAPTURE_PHASE = 1 << 2;

// IS_PASSIVE:将 1 左移 3 位,值为 8,二进制表示为 01000。表示事件监听器是否为被动监听器。
export const IS_PASSIVE = 1 << 3;

// IS_LEGACY_FB_SUPPORT_MODE:将 1 左移 4 位,值为 16,二进制表示为 10000。用于表示是否启用旧版 Facebook 支持模式。
export const IS_LEGACY_FB_SUPPORT_MODE = 1 << 4;

// IS_LEGACY_FB_SUPPORT_MODE 的二进制表示是 10000,IS_CAPTURE_PHASE 的二进制表示是 00100,按位或运算后得到 10100,对应的十进制值为 20。这个标志表示在旧版 Facebook 支持模式下且处于捕获阶段时,点击事件不应该被延迟处理。
export const SHOULD_NOT_DEFER_CLICK_FOR_FB_SUPPORT_MODE =
  IS_LEGACY_FB_SUPPORT_MODE | IS_CAPTURE_PHASE;

事件传播机制

在 DOM 事件模型中,事件的传播分为三个阶段:捕获阶段、目标阶段和冒泡阶段。

  • 捕获阶段:事件从文档根节点开始,依次向下传播到触发事件的具体元素。
  • 目标阶段:事件到达触发事件的目标元素。
  • 冒泡阶段:事件从目标元素开始,依次向上传播到文档根节点。

在添加事件监听器时,可以通过设置 capture 参数为 truefalse 来指定监听器是在捕获阶段还是冒泡阶段触发。因此,在移除事件监听器时,也需要传入 capture 参数来明确要移除的是哪个阶段的监听器。

被动监听

在JavaScript中,被动监听(Passive Event Listeners)是一种事件监听机制,主要用于优化页面的滑动性能,特别是在移动设备上。

被动监听器(Passive Event Listeners)告诉浏览器该事件监听器是否会调用preventDefault函数来阻止事件的默认行为。

当事件监听器的passive属性设置为true时,表示该监听器不会调用preventDefault来阻止默认行为,这样浏览器可以更高效地处理滚动和其他交互事件,从而提升页面性。

委托事件与非委托事件

事件委托(Event Delegation)非委托事件(直接绑定) 是两种处理 DOM 事件的基本模式。

它们的核心区别体现在 事件绑定位置执行机制性能优化 三个方面:

1. 事件绑定位置

  • 非委托事件:直接绑定到目标元素上
// 为每个按钮单独绑定点击事件
document.querySelectorAll('button').forEach(button => {
  button.addEventListener('click', handleClick);
});
  • 委托事件:绑定到父元素或根元素,利用事件冒泡机制捕获子元素事件
// 在父容器上统一处理所有按钮点击事件
document.querySelector('div').addEventListener('click', (e) => {
  if (e.target.tagName === 'BUTTON') {
    handleClick(e);
  }
});

2. 执行机制

非委托事件

  • 每个元素独立响应:每个绑定元素都有独立的事件处理函数
  • 事件流路径完整:从 window 到目标元素,再冒泡回 window
  • 开销与元素数量成正比:绑定元素越多,内存占用和事件处理成本越高

委托事件

  • 统一处理,动态匹配:事件在父元素上被捕获,通过 e.target 判断实际触发元素
  • 事件流优化:仅需在父元素上维护一个事件监听器
  • 动态元素自动生效:新增的子元素无需重新绑定事件

 
3. 性能与维护性

事件监听器(捕获与冒泡触发时机)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #box {
        height: 200px;
        width: 500px;
        background-color: orange;
      }
      #content {
        height: 50px;
        width: 50px;
        background-color: aqua;
        margin: 10px;
      }
    </style>
  </head>

  <body>
    <p>事件监听器</p>
    <div id="box">
      <div id="content"></div>
    </div>
  </body>

  <script>
    const box = document.getElementById("box");
    const content = document.getElementById("content");

    box.addEventListener(
      "click",
      (e) => {
        console.log("box-click", e);
      },
      true
    );

    content.addEventListener(
      "click",
      (e) => {
        console.log("content-click", e);
      },
      true
    );
  </script>
</html>

操作:点击 content 块。

1、情况(设置捕获)

box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  },
  true
);

content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  },
  true
);
box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  },
  true
);

content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  },
  true
);

代码打印结果一致:

2、默认情况(Chrome 浏览器 冒泡)

box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  }
);

content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  }
);

3、情况(设置冒泡)

box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  },
  false
);

content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  },
  false
);

4、情况(一个设置捕获、一个设置冒泡)

content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  },
  false
);
box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  },
  true
);

box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  },
  true
);
content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  },
  false
);

注册顺序不同、打印结果一致:(可以看出捕获的先行执行)

5、情况(一个设置捕获、一个设置冒泡)

content.addEventListener(
  "click",
  (e) => {
    console.log("content-click", e);
  },
  true
);
box.addEventListener(
  "click",
  (e) => {
    console.log("box-click", e);
  },
  false
);

注册顺序不同、打印结果一致:(可以看出捕获的先行执行)

不可打印字符

不可打印字符(Non-printable Character)是指在计算机字符集中 无法通过常规打印机或显示器显示为可见符号 的字符。它们通常用于控制文本的格式、传输协议或系统功能,而非直接呈现给用户。

特点

  1. 不可见性:在文本中显示为空白或特殊符号(如 \n\t),但本质上不占用可视化空间。
  2. 功能性:用于文本格式化、设备控制或数据传输,例如:
    • 换行符(\n)控制文本换行
    • 传输结束符(ETX)标记数据传输终止
    • 制表符(\t)实现段落缩进

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值