编写事件监听函数

89 阅读4分钟

一、基础实现:基于原生addEventListener

核心是使用浏览器原生API,封装一个能绑定事件、支持多种参数的基础函数:

/**
 * 基础事件监听函数
 * @param {HTMLElement} el - 目标DOM元素
 * @param {string} type - 事件类型(如'click'、'scroll')
 * @param {Function} handler - 事件处理函数
 * @param {boolean|Object} options - 事件选项(捕获/冒泡、once等)
 */
function on(el, type, handler, options = false) {
  // 校验参数合法性
  if (!el || !type || typeof handler !== 'function') {
    throw new Error('参数不合法');
  }
  // 绑定事件
  el.addEventListener(type, handler, options);
  
  // 返回解绑函数(方便后续移除监听)
  return () => {
    el.removeEventListener(type, handler, options);
  };
}

使用示例

const btn = document.querySelector('button');
// 绑定点击事件(冒泡阶段)
const off = on(btn, 'click', (e) => {
  console.log('按钮被点击');
});
// 如需解绑:off();

二、进阶功能:支持事件委托

针对动态生成的元素(如列表项),需实现事件委托(利用事件冒泡,通过父元素代理子元素事件):

/**
 * 支持事件委托的监听函数
 * @param {HTMLElement} parent - 父元素(事件委托的载体)
 * @param {string} type - 事件类型
 * @param {Function} handler - 处理函数(接收子元素和事件对象)
 * @param {string} selector - 子元素选择器(如'li.item')
 */
function onDelegate(parent, type, handler, selector) {
  if (!parent || !type || !handler || !selector) {
    throw new Error('参数不合法');
  }
  
  const delegateHandler = (e) => {
    // 找到匹配selector的子元素( closest:从目标向上查找最近匹配的祖先)
    const target = e.target.closest(selector);
    if (target) {
      // 执行处理函数,传入目标子元素和事件对象
      handler.call(target, e, target);
    }
  };
  
  // 绑定到父元素,返回解绑函数
  parent.addEventListener(type, delegateHandler);
  return () => {
    parent.removeEventListener(type, delegateHandler);
  };
}

使用示例(列表项点击):

const list = document.querySelector('ul');
// 委托ul监听li的点击
onDelegate(list, 'click', (e, target) => {
  console.log('点击了列表项:', target.textContent);
}, 'li'); // 子元素选择器

三、兼容性处理(兼容旧浏览器)

针对不支持addEventListener的旧浏览器(如IE8及以下),需兼容attachEvent

function on(el, type, handler, options = false) {
  if (!el || !type || typeof handler !== 'function') {
    throw new Error('参数不合法');
  }
  
  // 兼容IE8-:使用attachEvent(仅支持冒泡,事件类型需加'on'前缀)
  if (el.attachEvent) {
    const ieType = 'on' + type;
    const ieHandler = function() {
      // 修复IE下event对象的指向
      handler.call(el, window.event);
    };
    el.attachEvent(ieType, ieHandler);
    return () => {
      el.detachEvent(ieType, ieHandler);
    };
  }
  
  // 现代浏览器:使用addEventListener
  el.addEventListener(type, handler, options);
  return () => {
    el.removeEventListener(type, handler, options);
  };
}

四、功能扩展:支持一次性事件、事件节流/防抖

实际开发中,常需要扩展功能,例如支持“只执行一次的事件”“节流/防抖处理”:

/**
 * 增强版事件监听:支持一次性、节流、防抖
 * @param {Object} options - 扩展选项:{ once, throttle, debounce }
 */
function onAdvanced(el, type, handler, {
  once = false,
  throttle = 0, // 节流时间(ms)
  debounce = 0, // 防抖时间(ms)
  capture = false
} = {}) {
  let wrappedHandler = handler;
  
  // 防抖处理
  if (debounce > 0) {
    let timer;
    wrappedHandler = function(...args) {
      clearTimeout(timer);
      timer = setTimeout(() => handler.apply(this, args), debounce);
    };
  }
  
  // 节流处理
  if (throttle > 0) {
    let lastTime = 0;
    wrappedHandler = function(...args) {
      const now = Date.now();
      if (now - lastTime > throttle) {
        lastTime = now;
        handler.apply(this, args);
      }
    };
  }
  
  // 一次性事件
  if (once) {
    const onceHandler = function(...args) {
      wrappedHandler.apply(this, args);
      off(); // 执行后立即解绑
    };
    wrappedHandler = onceHandler;
  }
  
  // 绑定事件
  el.addEventListener(type, wrappedHandler, capture);
  const off = () => {
    el.removeEventListener(type, wrappedHandler, capture);
  };
  return off;
}

使用示例(滚动事件节流):

// 滚动事件节流(500ms执行一次)
onAdvanced(window, 'scroll', () => {
  console.log('滚动位置:', window.scrollY);
}, { throttle: 500 });

五、问题

1. 问:为什么要封装事件监听函数,而不直接用addEventListener
  • :封装可统一处理参数校验、兼容性、功能扩展(如节流、委托),减少重复代码,提高可维护性。例如在大型项目中,统一的事件工具能保证团队代码风格一致。
2. 问:事件委托的核心原理是什么?为什么用closest而不是target直接判断?
  • :事件委托基于事件冒泡,让父元素代理子元素事件。用closest(selector)可处理“子元素有嵌套结构”的场景(如li内有span,点击span时仍能找到li),比直接判断target更灵活。
3. 问:如何避免事件监听内存泄漏?
    • 组件卸载时主动调用解绑函数(如React的useEffect返回解绑);
    • 避免在事件处理函数中引用外部大对象,或使用弱引用(WeakMap)存储关联关系。

六、总结

“编写事件监听函数的核心是基于原生API封装,兼顾功能完整性和工程化需求:

  1. 基础版用addEventListener,支持参数校验和解绑;
  2. 进阶版加入事件委托,利用冒泡让父元素代理动态子元素事件;
  3. 兼容旧浏览器需处理attachEvent,修复IE的事件对象问题;
  4. 扩展版支持节流、防抖、一次性事件,满足复杂场景(如滚动、输入框搜索)。