香草JavaScript事件委托
Vanilla JavaScript Event Delegation
在香草js中做事件委托的最佳方式(最快/适当)是什么?
例如,如果我在jQuery中这样写:
$('#main').on('click', '.focused', function(){
settingsPanel();
});
我如何将其转换为香草js?也许是.addEventListener()
我能想到的方法是:
document.getElementById('main').addEventListener('click', dothis);
function dothis(){
// now in jQuery
$(this).children().each(function(){
if($(this).is('.focused') settingsPanel();
});
}
但是,如果#main
有很多孩子,这似乎是低效的。
这样做合适吗?
document.getElementById('main').addEventListener('click', doThis);
function doThis(event){
if($(event.target).is('.focused') || $(event.target).parents().is('.focused') settingsPanel();
}
与其改变内置的原型(这会导致脆弱的代码,并且经常会破坏东西),不如检查点击的元素是否有一个与你想要的选择器匹配的.closest
元素。如果是,则调用要调用的函数。例如,要翻译
$('#main').on('click', '.focused', function(){
settingsPanel();
});
脱离jQuery,使用:
document.querySelector('#main').addEventListener('click', (e) => {
if (e.target.closest('#main .focused')) {
settingsPanel();
}
});
除非内部选择器也可以作为父元素存在(这可能非常不寻常),否则将内部选择器单独传递给.closest
(例如.closest('.focused')
)就足够了。
在使用这种模式时,为了保持简洁,我经常将代码的主要部分放在早期返回的下面,例如:
document.querySelector('#main').addEventListener('click', (e) => {
if (!e.target.closest('.focused')) {
return;
}
// code of settingsPanel here, if it isn't too long
});
现场演示:
document.querySelector('#outer').addEventListener('click', (e) => {
if (!e.target.closest('#inner')) {
return;
}
console.log('vanilla');
});
$('#outer').on('click', '#inner', () => {
console.log('jQuery');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="outer">
<div id="inner">
inner
<div id="nested">
nested
</div>
</div>
</div>
我已经提出了一个简单的解决方案似乎工作得相当好(尽管传统的IE支持)。这里我们扩展EventTarget
的原型,提供一个delegateEventListener
方法,它使用以下语法工作:
EventTarget.delegateEventListener(string event, string toFind, function fn)
我创建了一个相当复杂的fiddle来演示它的实际操作,其中我们将所有事件委托给绿色元素。停止传播继续工作,您可以通过this
访问event.currentTarget
(与jQuery一样)。
以下是完整的解决方案:
(function(document, EventTarget) {
var elementProto = window.Element.prototype,
matchesFn = elementProto.matches;
/* Check various vendor-prefixed versions of Element.matches */
if(!matchesFn) {
['webkit', 'ms', 'moz'].some(function(prefix) {
var prefixedFn = prefix + 'MatchesSelector';
if(elementProto.hasOwnProperty(prefixedFn)) {
matchesFn = elementProto[prefixedFn];
return true;
}
});
}
/* Traverse DOM from event target up to parent, searching for selector */
function passedThrough(event, selector, stopAt) {
var currentNode = event.target;
while(true) {
if(matchesFn.call(currentNode, selector)) {
return currentNode;
}
else if(currentNode != stopAt && currentNode != document.body) {
currentNode = currentNode.parentNode;
}
else {
return false;
}
}
}
/* Extend the EventTarget prototype to add a delegateEventListener() event */
EventTarget.prototype.delegateEventListener = function(eName, toFind, fn) {
this.addEventListener(eName, function(event) {
var found = passedThrough(event, toFind, event.currentTarget);
if(found) {
// Execute the callback with the context set to the found element
// jQuery goes way further, it even has it's own event object
fn.call(found, event);
}
});
};
}(window.document, window.EventTarget || window.Element));
我有一个类似的解决方案来实现事件委托。它使用了数组函数slice
, reverse
, filter
和forEach
。
-
slice
将NodeList从查询转换为数组,这必须在允许反转列表之前完成。 -
reverse
反转数组(使最终的迁移开始尽可能靠近事件目标)。 -
filter
检查哪些元素包含event.target
。 -
forEach
为过滤结果中的每个元素调用提供的处理程序,只要处理程序不返回false
。
函数返回创建的委托函数,这使得以后可以删除侦听器。请注意,本机event.stopPropagation()
不会停止通过validElements
的遍历,因为冒泡阶段已经遍历到委托元素。
function delegateEventListener(element, eventType, childSelector, handler) {
function delegate(event){
var bubble;
var validElements=[].slice.call(this.querySelectorAll(childSelector)).reverse().filter(function(matchedElement){
return matchedElement.contains(event.target);
});
validElements.forEach(function(validElement){
if(bubble===undefined||bubble!==false)bubble=handler.call(validElement,event);
});
}
element.addEventListener(eventType,delegate);
return delegate;
}
虽然不建议扩展原生原型,但可以将此函数添加到EventTarget
(或IE中的Node
)的原型中。此时,将函数内的element
替换为this
,并删除相应的参数(EventTarget.prototype.delegateEventListener = function(eventType, childSelector, handler){...}
)。
委托事件
事件委托在需要执行一个函数时使用,当存在的或动态的元素(将来添加到DOM中)接收到一个事件。
该策略是将事件侦听器分配给已知的静态父节点,并遵循以下规则:
-
使用
evt.target.closest(".dynamic")
获取所需的动态子节点 - 使用
evt.currentTarget
获取#staticParent
父节点委托节点 - 使用
evt.target
获得确切的单击元素(警告!)这也可以是一个后代元素,不一定是.dynamic
那个)
片段示例:
document.querySelector("#staticParent").addEventListener("click", (evt) => {
const elChild = evt.target.closest(".dynamic");
if ( !elChild ) return; // do nothing.
console.log("Do something with elChild Element here");
});
完整的动态元素示例:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Delegated events
el("#staticParent").addEventListener("click", (evt) => {
const elDelegator = evt.currentTarget;
const elChild = evt.target.closest(".dynamicChild");
const elTarget = evt.target;
console.clear();
console.log(`Clicked:
currentTarget: ${elDelegator.tagName}
target.closest: ${elChild?.tagName}
target: ${elTarget.tagName}`)
if (!elChild) return; // Do nothing.
// Else, .dynamicChild is clicked! Do something:
console.log("Yey! .dynamicChild is clicked!")
});
// Insert child element dynamically
setTimeout(() => {
el("#staticParent").append(elNew("article", {
className: "dynamicChild",
innerHTML: `Click here!!! I'm added dynamically! <span>Some child icon</span>`
}))
}, 1500);
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 1rem;
}
.dynamicChild span {
background: gold;
padding: 0.5rem;
}
<section id="staticParent">Click here or...</section>
直接事件或者,您可以在创建时直接在子对象上附加一个click处理程序:
// DOM utility functions:
const el = (sel, par) => (par || document).querySelector(sel);
const elNew = (tag, prop) => Object.assign(document.createElement(tag), prop);
// Create new comment with Direct events:
const newComment = (text) => elNew("article", {
className: "dynamicChild",
title: "Click me!",
textContent: text,
onclick() {
console.log(`Clicked: ${this.textContent}`);
},
});
//
el("#add").addEventListener("click", () => {
el("#staticParent").append(newComment(Date.now()))
});
#staticParent {
border: 1px solid #aaa;
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.dynamicChild {
background: #eee;
padding: 0.5rem;
}
<section id="staticParent"></section>
<button type="button" id="add">Add new</button>
资源:
- Event.target
- Element.closest () jQuery vs JavaScript
- 召回窗口加载事件 - javascript
- 动态创建OnClick事件Javascript
- 全局屏幕span onclick触发一个事件javascript
- 使用react js在跨域上执行事件javascript
- 在change事件javascript上动态添加行
- 在粘贴事件Javascript的输入字段中删除所有非数字字符
- 如何在多表单的输入框中使用输入事件javascript
- 无法查看特定信息.单击事件.Javascript
- 两个文本框事件Javascript
- 阻止鼠标滚轮滚动,但不阻止滚动条事件.JavaScript
- 从 onkeydown 事件 (Javascript) 获取当前用户输入
- “稍后提醒我”/“快速事件”JavaScript解决方案
- 更改我正在侦听的事件Javascript的对象的CSS
- setInterval 不允许同时单击事件 - javascript
- 对每种情况使用一个事件(JavaScript 开关)
- 仅创建第二次点击事件 Javascript
- 每 60 秒仅启动一次事件 (JavaScript)
- 如何连接来自不同文件的事件?Javascript/Backbone
- URL 更改事件 - JavaScript
- 无法在无头模式下使用 watir Webdriver 执行按钮元素的 onclick 事件 javascript