事件高级

事件委托

利用事件冒泡机制,在父元素上统一处理子元素的事件。

基本概念

<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事件监听器

// passive: true 表示不会调用preventDefault
// 浏览器可以优化滚动性能
document.addEventListener("touchstart", function(e) {
    // 这里不能阻止默认行为
    // e.preventDefault() 会被忽略
}, { passive: true });

// 需要阻止默认行为时,不要用passive
document.addEventListener("touchstart", function(e) {
    e.preventDefault();  // 可以正常工作
}, { passive: false });

阻止事件传播

使用stopPropagation和stopImmediatePropagation方法。

stopPropagation

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只输出 "子元素"

stopImmediatePropagation

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次(最后一次)

下一步

掌握了事件高级处理后,让我们继续学习:

  1. Window对象 - 学习BOM基础
  2. Location对象 - 学习URL操作
  3. History对象 - 学习历史记录管理

东巴文(db-w.cn) - 让编程学习更简单

🎯 东巴文寄语:事件委托、节流防抖、自定义事件是前端开发中的高级技巧,掌握这些技术可以显著提升应用性能和代码质量。在 db-w.cn,我们帮你成为事件处理专家!