一、基础实现:基于原生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)存储关联关系。
- 组件卸载时主动调用解绑函数(如React的
六、总结
“编写事件监听函数的核心是基于原生API封装,兼顾功能完整性和工程化需求:
- 基础版用
addEventListener,支持参数校验和解绑; - 进阶版加入事件委托,利用冒泡让父元素代理动态子元素事件;
- 兼容旧浏览器需处理
attachEvent,修复IE的事件对象问题; - 扩展版支持节流、防抖、一次性事件,满足复杂场景(如滚动、输入框搜索)。