实现一个 JS delegate 事件委托

DebugMi 发布于 2021-11-14 00:39编辑于 2024-08-29 03:45阅读:

今天写了一个平台的 SDK,SDK 不复杂,优先级还很高,必须得在 head 无 defer 地插入,所以体积需要尽可能地小,以免影响页面加载性能

不依赖任何库写前端,都逃不过一个问题:「事件委托」,也叫「事件代理」,依赖场景有:

  • 避免循环定义事件导致的性能问题
  • 异步插入的 DOM 无法定义事件(DOM 上直接写 onxxx 除外)

先设计 API

delegate("#haha", "click", (e) => console.log(e));

简单实现一下

先定义一个 map,key 为事件类型,value 为绑定的元素数组。因为同一个事件只需要在 document 上监听一次,避免浪费

type DelegateEventMap = Record<
  keyof DocumentEventMap,
  Array<{
    selector: string;
    handler: (e: Event) => any;
  }>
>;

const delegateEventMap = {} as DelegateEventMap;

/**
 * 委托事件
 * @param selector 要委托的元素选择器
 * @param eventType 事件类型
 * @param handler 事件回调
 * @example delegate('#btn', 'click', (e) => console.log(e))
 */
function delegate(
  selector: string,
  eventType: keyof DocumentEventMap,
  handler: (e: Event) => any
) {
  if (!delegateEventMap.hasOwnProperty(eventType)) {
    document.addEventListener(eventType, handleDocument);
    delegateEventMap[eventType] = [{ selector, handler }];
  }
}

document 的事件回调,当获取到事件的 target 元素之后,如果不匹配,就一层层往它的父级寻找,因为可能点击的是他的父级,然后每次寻找都在 map 里看看当前元素对应,有就执行回调

// 向上寻找节点,直到找到或者到达顶部
function handleDocument(e: Event | null, dom?: HTMLElement) {
  if (!e && !dom) {
    return;
  }
  if (dom && dom.tagName === "HTML") {
    return;
  }

  const eventType = e.type;
  const selectorInfos = delegateEventMap[eventType] as
    | DelegateEventMap["click"]
    | undefined;

  if (!selectorInfos) {
    return;
  }

  const currentDom = dom || (e?.target as HTMLElement);
  const selectorInfo = selectorInfos.find((item) =>
    currentDom.matches?.(item.selector)
  );

  if (selectorInfo) {
    selectorInfo.handler(e);
    return;
  }

  const parentDom = currentDom.parentNode as HTMLElement;

  if (parentDom) {
    handleDocument(e, parentDom);
  }
}

以上,代码很少,大体实现了 jQuery 的 delegate 方法

0