利用事件冒泡机制,在父元素上统一处理子元素的事件。
<ul id="list">
<li>项目1</li>
<li>项目2</li>
<li>项目3</li>
</ul>
<script>
// 传统方式:给每个li绑定事件
const items = document.querySelectorAll("li");
items.forEach(item => {
item.addEventListener("click", function() {
console.log(this.textContent);
});
});
// 事件委托:在父元素上绑定
const list = document.getElementById("list");
list.addEventListener("click", function(e) {
if (e.target.tagName === "LI") {
console.log(e.target.textContent);
}
});
</script>
const list = document.getElementById("list");
// 优势1:减少事件监听器数量
// 只需要一个监听器,而不是N个
// 优势2:动态元素自动处理
list.addEventListener("click", function(e) {
if (e.target.tagName === "LI") {
console.log(e.target.textContent);
}
});
// 新添加的元素自动具有事件处理
const newLi = document.createElement("li");
newLi.textContent = "新项目";
list.appendChild(newLi); // 点击也能响应
// 优势3:内存占用更少
// 只有一个事件处理函数
// 通用的委托函数
function delegate(parent, selector, type, handler) {
parent.addEventListener(type, function(e) {
let target = e.target;
// 向上查找匹配的元素
while (target && target !== parent) {
if (target.matches(selector)) {
handler.call(target, e);
return;
}
target = target.parentNode;
}
});
}
// 使用
delegate(document.getElementById("list"), "li", "click", function(e) {
console.log(this.textContent);
});
// 支持复杂选择器
delegate(document.querySelector(".container"), ".btn.primary", "click", function() {
console.log("主按钮点击");
});
// 注意1:e.target可能是子元素
list.addEventListener("click", function(e) {
// 如果li内有span,e.target是span
// 需要向上查找
let li = e.target.closest("li");
if (li && this.contains(li)) {
console.log(li.textContent);
}
});
// 注意2:某些事件不冒泡
// focus, blur等事件不冒泡,需要使用捕获版本或focusin/focusout
// 注意3:阻止冒泡会影响委托
item.addEventListener("click", function(e) {
e.stopPropagation(); // 阻止冒泡,委托失效
});
事件代理是事件委托的另一种说法,核心思想相同。
// 表格行点击
document.querySelector("table").addEventListener("click", function(e) {
const row = e.target.closest("tr");
if (row) {
console.log("行ID:", row.dataset.id);
}
});
// 按钮组
document.querySelector(".btn-group").addEventListener("click", function(e) {
const btn = e.target.closest("button");
if (!btn) return;
const action = btn.dataset.action;
switch (action) {
case "save":
save();
break;
case "delete":
remove();
break;
case "cancel":
cancel();
break;
}
});
// 导航菜单
document.querySelector(".nav").addEventListener("click", function(e) {
const link = e.target.closest("a");
if (link && this.contains(link)) {
e.preventDefault();
navigate(link.href);
}
});
使用preventDefault方法阻止元素的默认行为。
// 阻止链接跳转
document.querySelector("a").addEventListener("click", function(e) {
e.preventDefault();
console.log("链接被点击,但不跳转");
});
// 阻止表单提交
document.querySelector("form").addEventListener("submit", function(e) {
e.preventDefault();
// AJAX提交
});
// 阻止右键菜单
document.addEventListener("contextmenu", function(e) {
e.preventDefault();
showCustomMenu(e.clientX, e.clientY);
});
// 阻止键盘默认行为
document.addEventListener("keydown", function(e) {
if (e.key === "s" && e.ctrlKey) {
e.preventDefault();
save();
}
});
element.addEventListener("click", function(e) {
// 检查是否可以阻止默认行为
if (e.cancelable) {
e.preventDefault();
}
// 检查是否已经阻止
if (e.defaultPrevented) {
console.log("默认行为已被阻止");
}
});
// passive: true 表示不会调用preventDefault
// 浏览器可以优化滚动性能
document.addEventListener("touchstart", function(e) {
// 这里不能阻止默认行为
// e.preventDefault() 会被忽略
}, { passive: true });
// 需要阻止默认行为时,不要用passive
document.addEventListener("touchstart", function(e) {
e.preventDefault(); // 可以正常工作
}, { passive: false });
使用stopPropagation和stopImmediatePropagation方法。
const parent = document.querySelector("#parent");
const child = document.querySelector("#child");
parent.addEventListener("click", function() {
console.log("父元素");
});
child.addEventListener("click", function(e) {
console.log("子元素");
e.stopPropagation(); // 阻止冒泡到父元素
});
// 点击child只输出 "子元素"
const button = document.querySelector("button");
button.addEventListener("click", function(e) {
console.log("处理程序1");
e.stopImmediatePropagation(); // 阻止后续所有处理程序
});
button.addEventListener("click", function(e) {
console.log("处理程序2"); // 不会执行
});
button.addEventListener("click", function(e) {
console.log("处理程序3"); // 不会执行
});
// 点击只输出 "处理程序1"
| 方法 | 效果 |
|---|---|
| preventDefault | 阻止默认行为,事件继续传播 |
| stopPropagation | 阻止事件传播,当前元素其他处理程序继续执行 |
| stopImmediatePropagation | 阻止事件传播,当前元素后续处理程序也不执行 |
// 组合使用
document.querySelector("a").addEventListener("click", function(e) {
e.preventDefault(); // 不跳转
e.stopPropagation(); // 不冒泡
// 执行自定义逻辑
loadPage(this.href);
});
使用JavaScript代码模拟触发事件。
const button = document.querySelector("button");
// 旧方式(已废弃但广泛支持)
const event = document.createEvent("MouseEvents");
event.initMouseEvent("click", true, true);
button.dispatchEvent(event);
// 现代方式
const clickEvent = new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window
});
button.dispatchEvent(clickEvent);
// 简写
button.click(); // 模拟点击
const input = document.querySelector("input");
// 创建键盘事件
const keyEvent = new KeyboardEvent("keydown", {
key: "Enter",
code: "Enter",
keyCode: 13,
bubbles: true,
cancelable: true,
ctrlKey: false,
shiftKey: false
});
input.dispatchEvent(keyEvent);
const form = document.querySelector("form");
// 模拟提交
const submitEvent = new Event("submit", {
bubbles: true,
cancelable: true
});
form.dispatchEvent(submitEvent);
// 模拟input事件
const inputEvent = new Event("input", {
bubbles: true
});
input.value = "新值";
input.dispatchEvent(inputEvent);
const element = document.querySelector(".target");
const mouseEvent = new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window,
clientX: 100,
clientY: 100,
button: 0
});
element.dispatchEvent(mouseEvent);
创建和触发自定义事件。
// 旧方式
const event1 = document.createEvent("CustomEvent");
event1.initCustomEvent("myevent", true, true, { data: "东巴文" });
// 现代方式
const event2 = new CustomEvent("myevent", {
bubbles: true,
cancelable: true,
detail: { data: "东巴文" }
});
// 监听自定义事件
document.addEventListener("myevent", function(e) {
console.log("自定义事件触发:", e.detail);
});
// 触发
document.dispatchEvent(event2);
// 组件通信
class Counter {
constructor(element) {
this.element = element;
this.count = 0;
}
increment() {
this.count++;
// 触发自定义事件
const event = new CustomEvent("countchange", {
bubbles: true,
detail: { count: this.count }
});
this.element.dispatchEvent(event);
}
}
// 使用
const counter = new Counter(document.querySelector("#counter"));
document.addEventListener("countchange", function(e) {
console.log("计数改变:", e.detail.count);
});
counter.increment();
// 简单的事件发射器
class EventEmitter {
constructor() {
this.events = {};
}
on(event, callback) {
if (!this.events[event]) {
this.events[event] = [];
}
this.events[event].push(callback);
return this;
}
off(event, callback) {
if (!this.events[event]) return this;
this.events[event] = this.events[event].filter(cb => cb !== callback);
return this;
}
emit(event, ...args) {
if (!this.events[event]) return this;
this.events[event].forEach(callback => callback(...args));
return this;
}
once(event, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(event, wrapper);
};
this.on(event, wrapper);
return this;
}
}
// 使用
const emitter = new EventEmitter();
emitter.on("message", function(data) {
console.log("收到消息:", data);
});
emitter.emit("message", "东巴文");
限制事件处理函数的执行频率。
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
// 使用
const throttledScroll = throttle(function() {
console.log("滚动位置:", window.scrollY);
}, 200);
window.addEventListener("scroll", throttledScroll);
function throttle(func, delay) {
let lastTime = 0;
return function(...args) {
const now = Date.now();
if (now - lastTime >= delay) {
func.apply(this, args);
lastTime = now;
}
};
}
// 特点:第一次立即执行,最后一次可能被忽略
function throttle(func, delay) {
let timer = null;
return function(...args) {
if (!timer) {
timer = setTimeout(() => {
func.apply(this, args);
timer = null;
}, delay);
}
};
}
// 特点:第一次延迟执行,最后一次一定会执行
function throttle(func, delay, options = {}) {
let timer = null;
let lastTime = 0;
const { leading = true, trailing = true } = options;
return function(...args) {
const now = Date.now();
if (!lastTime && !leading) {
lastTime = now;
}
const remaining = delay - (now - lastTime);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
lastTime = now;
func.apply(this, args);
} else if (!timer && trailing) {
timer = setTimeout(() => {
lastTime = leading ? Date.now() : 0;
timer = null;
func.apply(this, args);
}, remaining);
}
};
}
延迟执行,在事件停止触发后才执行。
function debounce(func, delay) {
let timer = null;
return function(...args) {
clearTimeout(timer);
timer = setTimeout(() => {
func.apply(this, args);
}, delay);
};
}
// 使用
const debouncedSearch = debounce(function(keyword) {
console.log("搜索:", keyword);
}, 300);
input.addEventListener("input", function() {
debouncedSearch(this.value);
});
function debounce(func, delay, immediate = false) {
let timer = null;
return function(...args) {
const callNow = immediate && !timer;
clearTimeout(timer);
timer = setTimeout(() => {
timer = null;
if (!immediate) {
func.apply(this, args);
}
}, delay);
if (callNow) {
func.apply(this, args);
}
};
}
// 使用
const debouncedSave = debounce(save, 1000, true); // 立即执行第一次
function debounce(func, delay, options = {}) {
let timer = null;
let lastArgs = null;
let lastThis = null;
const { leading = false, trailing = true, maxWait } = options;
let maxTimer = null;
function invokeFunc() {
func.apply(lastThis, lastArgs);
lastArgs = lastThis = null;
}
function startTimer(pendingFunc, wait) {
return setTimeout(pendingFunc, wait);
}
return function(...args) {
lastArgs = args;
lastThis = this;
const shouldCallNow = leading && !timer;
if (timer) clearTimeout(timer);
if (maxTimer) clearTimeout(maxTimer);
timer = startTimer(() => {
timer = null;
if (trailing && lastArgs) {
invokeFunc();
}
}, delay);
if (maxWait && !maxTimer) {
maxTimer = startTimer(() => {
maxTimer = null;
if (lastArgs) {
invokeFunc();
}
}, maxWait);
}
if (shouldCallNow) {
invokeFunc();
}
};
}
// 节流:固定时间间隔执行
// 鼠标移动、滚动事件
// 防抖:停止触发后执行
// 搜索输入、窗口resize
// 示例对比
const throttled = throttle(log, 1000);
const debounced = debounce(log, 1000);
// 连续触发10秒
// 节流:执行约10次
// 防抖:只执行1次(最后一次)
掌握了事件高级处理后,让我们继续学习:
东巴文(db-w.cn) - 让编程学习更简单
🎯 东巴文寄语:事件委托、节流防抖、自定义事件是前端开发中的高级技巧,掌握这些技术可以显著提升应用性能和代码质量。在 db-w.cn,我们帮你成为事件处理专家!