详解js的事件代理(委托)
本文深入解析JavaScript事件代理(委托)机制,阐述其通过祖先元素监听子元素事件以减少注册数量、优化内存及支持动态DOM绑定的优势。通过原生JS代码逐步演示核心实现,利用apply改变this指向,并进一步封装为类似jQuery的简洁API,帮助开发者高效处理复杂DOM交互。
事件代理的基本概念与优势
在JavaScript开发中,事件代理(也称为事件委托)通常应用于特定的场景。
首先,将事件监听器注册在父级或祖先元素上,由其代为处理子元素的事件。这种方式能够显著减少页面上事件监听器的注册数量,从而降低内存消耗并提升整体性能。
其次,对于通过JavaScript动态添加的子元素,使用事件代理可以自动继承事件绑定,无需为每个新元素单独注册监听器。
原生JS实现事件代理
以往开发者常依赖jQuery等库提供的便捷方法来实现事件代理,操作十分简便。本文将尝试使用原生JavaScript代码手动实现这一功能。
var addEvent = (function () {
if (document.addEventListener) {
return function (element, type, handler) {
element.addEventListener(type, handler, false);
};
} else if (document.attachEvent) {
return function (element, type, handler) {
element.attachEvent('on' + type, function () {
handler.apply(element, arguments);
});
};
} else {
return function (element, type, handler) {
element['on' + type] = function () {
return handler.apply(element, arguments);
};
};
}
})(),
getClassElements = function (parentElement, classname) {
var all, element, classArr = [], classElements = [];
if (parentElement.getElementsByClassName) {
return parentElement.getElementsByClassName(classname);
} else {
all = parentElement.getElementsByTagName('*');
for (var i = 0, len = all.length; i < len; i++) {
element = all[i];
classArr = element && element.className && element.className.split(' ');
if (classArr) {
for (var j = 0; j < classArr.length; j++) {
if (classArr[j] === classname) {
classElements.push(element);
}
}
}
}
return classElements;
}
},
delegate = function () { // 参数:element, type, [selector,] handler
var args = arguments,
element = args[0],
type = args[1],
handler;
if (args.length === 3) {
handler = args[2];
return addEvent(element, type, handler);
}
if (args.length === 4) {
selector = args[2];
handler = args[3];
return addEvent(element, type, function (event) {
var event = event || window.event,
target = event.target || event.srcElement,
quickExpr = /^(?:[a-zA-Z]*#([\w-]+)|(\w+)|[a-zA-Z]*\.([\w-]+))$/,
match,
idElement,
elements,
tagName,
count = 0,
len;
if (typeof selector === 'string') {
match = quickExpr.exec(selector);
if (match) {
// #ID selector
if (match[1]) {
idElement = document.getElementById(match[1]);
tagName = match[0].slice(0, match[0].indexOf('#'));
// tag selector
} else if (match[2]) {
elements = element.getElementsByTagName(selector);
// .class selector
} else if (match[3]) {
elements = getClassElements(element, match[3]);
tagName = match[0].slice(0, match[0].indexOf('.'));
}
}
if (idElement) {
if ( tagName ? tagName === idElement.nodeName.toLowerCase() && target === idElement : target === idElement ) {
return handler.apply(idElement, arguments);
}
} else if (elements) {
for (len = elements.length; count < len; count++) {
if ( tagName ? tagName === elements[count].nodeName.toLowerCase() && target === elements[count] : target === elements[count] ) {
return handler.apply(elements[count], arguments);
}
}
}
}
});
}
};
实现的核心思路主要是利用apply方法改变函数执行时this的指向,使其指向实际触发事件的元素。
handler.apply(idElement, arguments);
handler.apply(elements[count], arguments);
接下来进行基础功能的测试验证。
<style>
#outer {padding: 50px; background-color: lightpink;}
#inner {padding: 30px; background-color: aliceblue;}
#paragraph1, #paragraph3 {background-color: cadetblue}
</style>
<div id="outer">outer
<div id="inner">inner
<p id="paragraph1" class="parag1">paragraph1</p>
<p id="paragraph2" class="parag">paragraph2</p>
<span>span</span>
<p id="paragraph3" class="parag">paragraph3</p>
</div>
</div>
var outer = document.getElementById('outer');
delegate(outer, 'click', function () {
console.log(this.id); // outer
});
delegate(outer, 'click', 'p', function () {
console.log(this.id); //点击 paragraph1 元素,输出其id为 "paragraph1"
});
jQuery风格封装与优化
为了提升代码的易用性,我们模仿jQuery的API风格对代码进行优化封装。
(function () {
var $ = function (element) {
return new _$(element);
};
var _$ = function (element) {
this.element = element && element.nodeType === 1 ? element : document;
};
_$.prototype = {
constructor: _$,
addEvent: function (type, handler, useCapture) {
var element = this.element;
if (document.addEventListener) {
element.addEventListener(type, handler, (useCapture ? useCapture : false));
} else if (document.attachEvent) {
element.attachEvent('on' + type, function () {
handler.apply(element, arguments);
});
} else {
element['on' + type] = function () {
return handler.apply(element, arguments);
};
}
return this;
},
getClassElements: function (classname) {
var element = this.element, all, ele, classArr = [], classElements = [];
if (element.getElementsByClassName) {
return element.getElementsByClassName(classname);
} else {
all = element.getElementsByTagName('*');
for (var i = 0, len = all.length; i < len; i++) {
ele = all[i];
classArr = ele && ele.className && ele.className.split(' ');
if (classArr) {
for (var j = 0; j < classArr.length; j++) {
if (classArr[j] === classname) {
classElements.push(ele);
}
}
}
}
return classElements;
}
},
delegate: function () { //参数:type, [selector,] handler
var self = this,
element = this.element,
type = arguments[0],
handler;
if (arguments.length === 2) {
handler = arguments[1];
return self.addEvent(type, handler);
} else if (arguments.length === 3) {
selector = arguments[1];
handler = arguments[2];
return self.addEvent(type, function (event) {
var event = event || window.event,
target = event.target || event.srcElement,
quickExpr = /^(?:[a-zA-Z]*#([\w-]+)|(\w+)|[a-zA-Z]*\.([\w-]+))$/,
match,
idElement,
elements,
tagName,
count = 0,
len;
if (typeof selector === 'string') {
match = quickExpr.exec(selector);
if (match) {
// #ID selector
if (match[1]) {
idElement = document.getElementById(match[1]);
tagName = match[0].slice(0, match[0].indexOf('#'));
// tag selector
} else if (match[2]) {
elements = element.getElementsByTagName(selector);
// .class selector
} else if (match[3]) {
elements = self.getClassElements(match[3]);
tagName = match[0].slice(0, match[0].indexOf('.'));
}
}
if (idElement) {
if ( tagName ? tagName === idElement.nodeName.toLowerCase() && target === idElement ? target === idElement ) {
return handler.apply(idElement, arguments);
}
} else if (elements) {
for (len = elements.length; count < len; count++) {
if ( tagName ? tagName === elements[count].nodeName.toLowerCase() && target === elements[count] : target === elements[count] ) {
return handler.apply(elements[count], arguments);
}
}
}
}
});
}
}
};
window.$ = $;
return $;
}());
优化后的代码调用方式如下所示。
var outer = document.getElementById('outer');
$(outer).delegate('click', '.parag', function (event) {
console.log(this.id);
});
以上内容涵盖了JavaScript事件代理的核心实现与应用,希望能为读者的学习或工作提供有益参考,欢迎继续支持。
anime.js 快速入门:怎么引入库并编写第一个 DOM 动画
2026-11-05 14:22:43
在线JS解密后变量名仍乱码?结合浏览器DevTools调试还原
2026-11-04 20:26:01
怎么在浏览器中运行JavaScript代码并查看输出结果
2026-11-04 09:07:33
JS 获取屏幕分辨率:window.screen.width 与 height 用法
2026-11-04 08:52:48
为什么推荐用DOMContentLoaded代替window.onload初始化JS
2026-11-04 08:42:33
