全局防抖方案的设计思路与实现:原型劫持的完整方案(零侵入)

有个历史遗留的老项目规范不够好,事件触发按钮较多且未作局部loading和防抖处理,当前任务是想给全局按钮自动添加防抖,用以优化用户体验和服务负载。

几种方案

  • 组件维度进行防抖设计,给当前交互对应的事件包裹防抖
  • 业务或者整个子应用可以进行自定义指令实现
  • 给当前使用的UI组件,二次封装使其拥有防抖功能
  • 对于我们这个,考虑全局劫持方案最优
    • 零侵入性:不需要修改任何现有代码
    • 全局覆盖:自动应用于所有按钮,包括动态生成的
    • 维护性好:集中管理,一处修改全局生效
    • 框架无关:纯JavaScript实现,不依赖特定框架

一、EventTarget 是一个 JavaScript 接口,表示可以接收事件的对象。

EventTarget 接口定义了三个主要的方法:

  • addEventListener():用于绑定事件监听器。
  • removeEventListener():用于移除已绑定的事件监听器。
  • dispatchEvent():用于分发事件,即触发事件。

二、识别目前元素,我们这个项目仅限制button类型

  • 验证当前是否为有效DOM元素element instanceof HTMLElement
  • 识别几种常用的按钮
javascript 复制代码
    function isButton(element) {
        return element instanceof HTMLElement && (
            element.tagName === 'BUTTON'
            || (element.tagName === 'INPUT' && ['button', 'submit'].includes(element.type))
            || element.getAttribute('role') === 'button'
        );
    }

三、防抖

javascript 复制代码
    function debounce(func, wait) {
        let timeout;
        return function (...args) {
            clearTimeout(timeout);
            timeout = setTimeout(() => func.apply(this, args), wait);
        };
    }
四、核心劫持逻辑
javascript 复制代码
  EventTarget.prototype.addEventListener = function (type, listener, options) {
    if (type === 'click' && isButton(this)) {
      // 避免重复创建防抖函数
      if (debounceMap.has(this)) {
        const buttonMap = debounceMap.get(this);
        if (buttonMap.has(listener)) {
          const existingDebounced = buttonMap.get(listener);
          return originalAddEventListener.call(this, type, existingDebounced, options);
        }
      }

      const debouncedListener = debounce(listener, delay);

      if (!debounceMap.has(this)) {
        debounceMap.set(this, new Map());
      }
      debounceMap.get(this).set(listener, debouncedListener);
      // 注册防抖后的监听器
      return originalAddEventListener.call(this, type, debouncedListener, options);
    }
    //非目标事件保持原样
    return originalAddEventListener.call(this, type, listener, options);
  };

五、完整示例及使用

  • 在项目入口文件main.js中进行使用即可
  • import { debounceClick } from './debounce-click.js';
JavaScript 复制代码
// 保存原始方法的全局变量
let originalAddEventListener;
let originalRemoveEventListener;

export function debounceClick(delay = 300) {
  if (window._eventTargetHijackEnabled) {
    console.warn('重复初始化');
    return;
  }

  // 备份原始方法
  if (!window._originalAddEventListener) {
    window._originalAddEventListener = EventTarget.prototype.addEventListener;
    window._originalRemoveEventListener = EventTarget.prototype.removeEventListener;
  }

  originalAddEventListener = window._originalAddEventListener;
  originalRemoveEventListener = window._originalRemoveEventListener;

  const debounceMap = new WeakMap();

  function isButton(element) {
    return (
      element instanceof HTMLElement &&
      (element.tagName === 'BUTTON' ||
        (element.tagName === 'INPUT' && ['button', 'submit'].includes(element.type)) ||
        element.getAttribute('role') === 'button')
    );
  }

  // 防抖函数
  function debounce(func, wait) {
    let timeout;
    return function (...args) {
      clearTimeout(timeout);
      timeout = setTimeout(() => func.apply(this, args), wait);
    };
  }

  // 劫持addEventListener
  EventTarget.prototype.addEventListener = function (type, listener, options) {
    if (type === 'click' && isButton(this)) {
      // 避免重复创建防抖函数
      if (debounceMap.has(this)) {
        const buttonMap = debounceMap.get(this);
        if (buttonMap.has(listener)) {
          const existingDebounced = buttonMap.get(listener);
          return originalAddEventListener.call(this, type, existingDebounced, options);
        }
      }

      const debouncedListener = debounce(listener, delay);

      if (!debounceMap.has(this)) {
        debounceMap.set(this, new Map());
      }
      debounceMap.get(this).set(listener, debouncedListener);

      return originalAddEventListener.call(this, type, debouncedListener, options);
    }
    return originalAddEventListener.call(this, type, listener, options);
  };

  // 劫持removeEventListener,不然会导致无法移除劫持处理后的事件监听器
  EventTarget.prototype.removeEventListener = function (type, listener, options) {
    if (type === 'click' && isButton(this) && debounceMap.has(this)) {
      const buttonMap = debounceMap.get(this);

      if (buttonMap.has(listener)) {
        const debouncedListener = buttonMap.get(listener);
        buttonMap.delete(listener);

        if (buttonMap.size === 0) {
          debounceMap.delete(this);
        }

        return originalRemoveEventListener.call(this, type, debouncedListener, options);
      }
    }
    return originalRemoveEventListener.call(this, type, listener, options);
  };

  window._eventTargetHijackEnabled = true;
  console.log(`劫持防抖启用(delay: ${delay}ms)`);
}

// 回滚原始
export function disableEventTargetHijack() {
  if (!window._eventTargetHijackEnabled) {
    console.warn('EventTarget劫持未启用,无需禁用');
    return;
  }

  // 恢复原始方法
  if (window._originalAddEventListener) {
    EventTarget.prototype.addEventListener = window._originalAddEventListener;
  }
  if (window._originalRemoveEventListener) {
    EventTarget.prototype.removeEventListener = window._originalRemoveEventListener;
  }

  // 清理所有无用的全局变量
  delete window._originalAddEventListener;
  delete window._originalRemoveEventListener;
  delete window._eventTargetHijackEnabled;

  console.warn('EventTarget劫持禁用完成');
}
相关推荐
柒和远方11 小时前
V079: Milvus 向量数据库:AI 日记本的三组件部署、集合字段建模与余弦索引
javascript·sql
水獭比特11 小时前
Anthropic Files / Skills 迁移:Workspace 不是租户隔离,API Key 也不是用户身份
javascript
默_笙11 小时前
☕ 我给 TS 类型做了台"瘦身手术",还顺手用 Docker 起了个 MySQL
前端·javascript
ji_shuke12 小时前
Vue 3 使用 History 哨兵和 popstate 拦截浏览器返回
前端·javascript·vue.js·history·哨兵·popstate
名字还没想好☜12 小时前
Prometheus 告警实战:写 alerting rules、用 Alertmanager 做路由分组与抑制
运维·前端·javascript·docker·kubernetes·prometheus
mONESY13 小时前
手把手从零复刻「英文网页 AI 翻译」Chrome 插件(新手向)
javascript
iFlyCai14 小时前
深入理解Flutter:StatefulWidget生命周期全解析(四)
前端·javascript·flutter
夜雨声烦丿14 小时前
从需求到页面:日期计算器应用的 ArkTS 原生实现
开发语言·javascript·华为·harmonyos
宇智波亚索15 小时前
TypeScript 实际应用
前端·javascript·typescript
:-)16 小时前
idea中的vue文件没有高亮显示
前端·javascript·vue.js·ecmascript·intellij-idea