Skip to content

JavaScript 事件

事件是 JavaScript 与用户交互的核心机制。通过事件,JavaScript 可以响应用户的操作(如点击、键盘输入等)以及其他浏览器行为(如页面加载、窗口大小改变等)。掌握事件处理是创建交互式网页应用的关键。在本章节中,我们将深入学习 JavaScript 中的事件系统。

什么是事件

事件是指在网页中发生的特定动作或状态变化,例如:

  • 用户点击按钮
  • 用户输入文本
  • 页面加载完成
  • 窗口大小改变
  • 鼠标移动
  • 键盘按键

JavaScript 可以监听这些事件并在事件发生时执行相应的代码。

事件类型

JavaScript 支持多种类型的事件:

1. 鼠标事件

javascript
// 常见鼠标事件
const button = document.getElementById("myButton");

button.addEventListener("click", function(event) {
    console.log("按钮被点击了");
});

button.addEventListener("dblclick", function(event) {
    console.log("按钮被双击了");
});

button.addEventListener("mousedown", function(event) {
    console.log("鼠标按键按下");
});

button.addEventListener("mouseup", function(event) {
    console.log("鼠标按键释放");
});

button.addEventListener("mouseover", function(event) {
    console.log("鼠标悬停在按钮上");
});

button.addEventListener("mouseout", function(event) {
    console.log("鼠标离开按钮");
});

button.addEventListener("mousemove", function(event) {
    console.log("鼠标在按钮上移动");
});

2. 键盘事件

javascript
// 常见键盘事件
const input = document.getElementById("myInput");

input.addEventListener("keydown", function(event) {
    console.log("按键按下:" + event.key);
});

input.addEventListener("keyup", function(event) {
    console.log("按键释放:" + event.key);
});

input.addEventListener("keypress", function(event) {
    console.log("按键按住:" + event.key);
});

// 特定按键处理
input.addEventListener("keydown", function(event) {
    if (event.key === "Enter") {
        console.log("按下了回车键");
    }
    
    if (event.ctrlKey && event.key === "s") {
        console.log("按下了 Ctrl+S");
        event.preventDefault(); // 阻止默认行为
    }
});

3. 表单事件

javascript
// 表单相关事件
const form = document.getElementById("myForm");
const input = document.getElementById("myInput");

form.addEventListener("submit", function(event) {
    console.log("表单提交");
    event.preventDefault(); // 阻止表单默认提交
});

input.addEventListener("focus", function(event) {
    console.log("输入框获得焦点");
    event.target.style.borderColor = "blue";
});

input.addEventListener("blur", function(event) {
    console.log("输入框失去焦点");
    event.target.style.borderColor = "gray";
});

input.addEventListener("change", function(event) {
    console.log("输入框内容改变:" + event.target.value);
});

input.addEventListener("input", function(event) {
    console.log("输入框内容实时改变:" + event.target.value);
});

4. 窗口事件

javascript
// 窗口相关事件
window.addEventListener("load", function(event) {
    console.log("页面加载完成");
});

window.addEventListener("beforeunload", function(event) {
    console.log("页面即将卸载");
    // 可以显示确认对话框
    event.returnValue = "确定要离开此页面吗?";
});

window.addEventListener("resize", function(event) {
    console.log("窗口大小改变:" + window.innerWidth + "x" + window.innerHeight);
});

window.addEventListener("scroll", function(event) {
    console.log("页面滚动:" + window.scrollY);
});

5. 其他事件

javascript
// 文档事件
document.addEventListener("DOMContentLoaded", function(event) {
    console.log("DOM 加载完成");
});

// 拖拽事件
const draggable = document.getElementById("draggable");

draggable.addEventListener("dragstart", function(event) {
    console.log("开始拖拽");
    event.dataTransfer.setData("text/plain", "拖拽的数据");
});

draggable.addEventListener("dragend", function(event) {
    console.log("拖拽结束");
});

// 媒体事件
const video = document.getElementById("myVideo");

video.addEventListener("play", function(event) {
    console.log("视频开始播放");
});

video.addEventListener("pause", function(event) {
    console.log("视频暂停");
});

video.addEventListener("ended", function(event) {
    console.log("视频播放结束");
});

事件处理方式

1. HTML 内联事件处理器

html
<!-- 不推荐使用 -->
<button onclick="handleClick()">点击我</button>
<input onfocus="handleFocus()" onblur="handleBlur()">
javascript
function handleClick() {
    console.log("按钮被点击了");
}

function handleFocus() {
    console.log("输入框获得焦点");
}

function handleBlur() {
    console.log("输入框失去焦点");
}

2. DOM 属性事件处理器

javascript
const button = document.getElementById("myButton");

// 设置事件处理器
button.onclick = function(event) {
    console.log("按钮被点击了");
};

button.onmouseover = function(event) {
    console.log("鼠标悬停");
};

// 移除事件处理器
button.onclick = null;

3. addEventListener(推荐)

javascript
const button = document.getElementById("myButton");

// 添加事件监听器
function handleClick(event) {
    console.log("按钮被点击了");
}

button.addEventListener("click", handleClick);

// 可以为同一事件添加多个监听器
button.addEventListener("click", function(event) {
    console.log("第二个点击处理器");
});

// 移除事件监听器
button.removeEventListener("click", handleClick);

事件对象(Event Object)

事件处理器函数接收一个事件对象参数,包含事件的详细信息:

javascript
button.addEventListener("click", function(event) {
    console.log("事件类型:" + event.type);
    console.log("目标元素:" + event.target);
    console.log("当前目标:" + event.currentTarget);
    console.log("事件时间戳:" + event.timeStamp);
    
    // 鼠标事件特有属性
    if (event.type === "click") {
        console.log("鼠标位置 X:" + event.clientX);
        console.log("鼠标位置 Y:" + event.clientY);
        console.log("按键:" + event.button);
    }
    
    // 键盘事件特有属性
    if (event.type === "keydown") {
        console.log("按键:" + event.key);
        console.log("键码:" + event.keyCode);
        console.log("是否按下 Ctrl:" + event.ctrlKey);
        console.log("是否按下 Alt:" + event.altKey);
        console.log("是否按下 Shift:" + event.shiftKey);
    }
});

事件传播(Event Propagation)

事件传播分为三个阶段:

  1. 捕获阶段:事件从 document 向下传播到目标元素
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从目标元素向上传播到 document
html
<div id="parent">
    <button id="child">点击我</button>
</div>
javascript
const parent = document.getElementById("parent");
const child = document.getElementById("child");

// 捕获阶段监听器
parent.addEventListener("click", function(event) {
    console.log("父元素捕获阶段");
}, true); // 第三个参数为 true 表示捕获阶段

// 冒泡阶段监听器
parent.addEventListener("click", function(event) {
    console.log("父元素冒泡阶段");
}, false); // 默认为 false,表示冒泡阶段

child.addEventListener("click", function(event) {
    console.log("子元素点击");
});

// 输出顺序:
// 父元素捕获阶段
// 子元素点击
// 父元素冒泡阶段

阻止事件默认行为和传播

阻止默认行为

javascript
const link = document.getElementById("myLink");

link.addEventListener("click", function(event) {
    event.preventDefault(); // 阻止链接跳转
    console.log("链接被点击,但不会跳转");
});

const form = document.getElementById("myForm");

form.addEventListener("submit", function(event) {
    event.preventDefault(); // 阻止表单提交
    console.log("表单提交被阻止");
});

阻止事件传播

javascript
const parent = document.getElementById("parent");
const child = document.getElementById("child");

child.addEventListener("click", function(event) {
    console.log("子元素点击");
    event.stopPropagation(); // 阻止事件继续传播
});

parent.addEventListener("click", function(event) {
    console.log("父元素点击"); // 不会执行
});

立即阻止事件传播和默认行为

javascript
element.addEventListener("click", function(event) {
    console.log("元素点击");
    event.stopImmediatePropagation(); // 立即阻止事件传播和同级监听器执行
});

自定义事件

JavaScript 允许创建和触发自定义事件:

javascript
// 创建自定义事件
const customEvent = new CustomEvent("myCustomEvent", {
    detail: {
        message: "这是自定义事件的数据",
        timestamp: Date.now()
    },
    bubbles: true, // 是否冒泡
    cancelable: true // 是否可取消
});

// 监听自定义事件
document.addEventListener("myCustomEvent", function(event) {
    console.log("接收到自定义事件:", event.detail.message);
    console.log("时间戳:", event.detail.timestamp);
});

// 触发自定义事件
document.dispatchEvent(customEvent);

// 带参数的自定义事件
function createAndDispatchEvent(eventName, data) {
    const event = new CustomEvent(eventName, {
        detail: data
    });
    document.dispatchEvent(event);
}

// 使用示例
createAndDispatchEvent("userLogin", {
    username: "张三",
    loginTime: new Date()
});

事件委托(Event Delegation)

事件委托利用事件冒泡机制,将事件处理器绑定到父元素上:

html
<ul id="itemList">
    <li data-id="1">项目 1</li>
    <li data-id="2">项目 2</li>
    <li data-id="3">项目 3</li>
</ul>
javascript
const itemList = document.getElementById("itemList");

// 使用事件委托处理所有 li 元素的点击事件
itemList.addEventListener("click", function(event) {
    // 检查点击的是否是 li 元素
    if (event.target.tagName === "LI") {
        const itemId = event.target.dataset.id;
        console.log("点击了项目:" + itemId);
        
        // 执行相应操作
        handleItemClick(itemId);
    }
});

function handleItemClick(itemId) {
    console.log("处理项目点击,ID:" + itemId);
}

// 动态添加新项目
function addItem(text) {
    const newItem = document.createElement("li");
    newItem.textContent = text;
    newItem.dataset.id = Date.now();
    itemList.appendChild(newItem);
    // 新项目自动具有点击事件处理能力
}

事件性能优化

1. 节流(Throttling)

javascript
// 节流函数
function throttle(func, delay) {
    let timeoutId;
    let lastExecTime = 0;
    
    return function(...args) {
        const currentTime = Date.now();
        
        if (currentTime - lastExecTime > delay) {
            func.apply(this, args);
            lastExecTime = currentTime;
        }
    };
}

// 使用节流处理滚动事件
window.addEventListener("scroll", throttle(function(event) {
    console.log("滚动事件处理");
}, 100)); // 每100毫秒最多执行一次

2. 防抖(Debouncing)

javascript
// 防抖函数
function debounce(func, delay) {
    let timeoutId;
    
    return function(...args) {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
            func.apply(this, args);
        }, delay);
    };
}

// 使用防抖处理输入事件
const searchInput = document.getElementById("searchInput");

searchInput.addEventListener("input", debounce(function(event) {
    console.log("执行搜索:" + event.target.value);
    // 执行搜索操作
    performSearch(event.target.value);
}, 300)); // 300毫秒内没有新输入才执行

3. 事件监听器管理

javascript
// 事件监听器管理器
class EventManager {
    constructor() {
        this.listeners = new Map();
    }
    
    // 添加事件监听器
    add(element, eventType, handler, options = {}) {
        const key = this._getKey(element, eventType);
        
        if (!this.listeners.has(key)) {
            this.listeners.set(key, []);
        }
        
        element.addEventListener(eventType, handler, options);
        this.listeners.get(key).push({
            element: element,
            handler: handler,
            options: options
        });
    }
    
    // 移除事件监听器
    remove(element, eventType, handler) {
        const key = this._getKey(element, eventType);
        const listeners = this.listeners.get(key) || [];
        
        element.removeEventListener(eventType, handler);
        
        this.listeners.set(key, listeners.filter(listener => 
            listener.handler !== handler
        ));
    }
    
    // 移除所有监听器
    removeAll() {
        for (let [key, listeners] of this.listeners) {
            listeners.forEach(listener => {
                listener.element.removeEventListener(
                    listener.eventType, 
                    listener.handler, 
                    listener.options
                );
            });
        }
        this.listeners.clear();
    }
    
    // 生成唯一键
    _getKey(element, eventType) {
        return `${element.constructor.name}_${eventType}`;
    }
}

// 使用示例
const eventManager = new EventManager();

const button = document.getElementById("myButton");
const input = document.getElementById("myInput");

eventManager.add(button, "click", function() {
    console.log("按钮点击");
});

eventManager.add(input, "input", function() {
    console.log("输入改变");
});

// 清理所有事件监听器
// eventManager.removeAll();

事件最佳实践

1. 使用现代事件处理方式

javascript
// 推荐:使用 addEventListener
element.addEventListener("click", handleClick);

// 不推荐:使用内联事件处理器
// <button onclick="handleClick()">点击</button>

// 不推荐:使用 DOM 属性
// element.onclick = handleClick;

2. 合理使用事件委托

javascript
// 对于动态列表,使用事件委托
const list = document.getElementById("dynamicList");

list.addEventListener("click", function(event) {
    if (event.target.classList.contains("delete-btn")) {
        // 删除按钮点击
        const item = event.target.closest(".list-item");
        if (item) {
            item.remove();
        }
    }
});

3. 及时清理事件监听器

javascript
// 组件销毁时清理事件监听器
class Component {
    constructor() {
        this.handleClick = this.handleClick.bind(this);
        this.handleResize = this.handleResize.bind(this);
        
        this.init();
    }
    
    init() {
        this.button.addEventListener("click", this.handleClick);
        window.addEventListener("resize", this.handleResize);
    }
    
    destroy() {
        this.button.removeEventListener("click", this.handleClick);
        window.removeEventListener("resize", this.handleResize);
    }
    
    handleClick(event) {
        // 处理点击
    }
    
    handleResize(event) {
        // 处理窗口大小改变
    }
}

4. 事件处理函数的优化

javascript
// 避免在事件处理函数中创建新函数
const button = document.getElementById("myButton");

// 不好的做法
button.addEventListener("click", function(event) {
    setTimeout(function() {
        console.log("延迟执行");
    }, 1000);
});

// 好的做法
function delayedAction() {
    console.log("延迟执行");
}

button.addEventListener("click", function(event) {
    setTimeout(delayedAction, 1000);
});

实际应用示例

1. 模态框组件

javascript
class Modal {
    constructor(modalId) {
        this.modal = document.getElementById(modalId);
        this.overlay = this.modal.querySelector(".modal-overlay");
        this.closeButton = this.modal.querySelector(".modal-close");
        
        this.init();
    }
    
    init() {
        // 绑定事件监听器
        this.overlay.addEventListener("click", () => this.close());
        this.closeButton.addEventListener("click", () => this.close());
        
        // ESC 键关闭
        document.addEventListener("keydown", (event) => {
            if (event.key === "Escape" && this.isOpen()) {
                this.close();
            }
        });
    }
    
    open() {
        this.modal.classList.add("show");
        document.body.classList.add("modal-open");
        this.dispatchCustomEvent("modal:open");
    }
    
    close() {
        this.modal.classList.remove("show");
        document.body.classList.remove("modal-open");
        this.dispatchCustomEvent("modal:close");
    }
    
    isOpen() {
        return this.modal.classList.contains("show");
    }
    
    dispatchCustomEvent(eventName) {
        const event = new CustomEvent(eventName, {
            detail: { modal: this }
        });
        this.modal.dispatchEvent(event);
    }
}

// 使用示例
const modal = new Modal("myModal");

// 监听自定义事件
document.getElementById("myModal").addEventListener("modal:open", function(event) {
    console.log("模态框打开");
});

document.getElementById("myModal").addEventListener("modal:close", function(event) {
    console.log("模态框关闭");
});

// 打开模态框
document.getElementById("openModal").addEventListener("click", function() {
    modal.open();
});

2. 表单验证系统

javascript
class FormValidator {
    constructor(formId) {
        this.form = document.getElementById(formId);
        this.fields = this.form.querySelectorAll("[data-validate]");
        this.errors = new Map();
        
        this.init();
    }
    
    init() {
        // 为每个字段添加验证事件
        this.fields.forEach(field => {
            const validateOn = field.dataset.validateOn || "blur";
            field.addEventListener(validateOn, () => this.validateField(field));
        });
        
        // 表单提交验证
        this.form.addEventListener("submit", (event) => {
            if (!this.validateForm()) {
                event.preventDefault();
                this.showErrors();
            }
        });
    }
    
    validateField(field) {
        const rules = field.dataset.validate.split(" ");
        const value = field.value.trim();
        let isValid = true;
        let errorMessage = "";
        
        for (let rule of rules) {
            switch (rule) {
                case "required":
                    if (!value) {
                        isValid = false;
                        errorMessage = "此字段为必填项";
                    }
                    break;
                    
                case "email":
                    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
                    if (value && !emailRegex.test(value)) {
                        isValid = false;
                        errorMessage = "请输入有效的邮箱地址";
                    }
                    break;
                    
                case "minLength":
                    const minLength = parseInt(field.dataset.minLength) || 0;
                    if (value && value.length < minLength) {
                        isValid = false;
                        errorMessage = `最少需要 ${minLength} 个字符`;
                    }
                    break;
            }
            
            if (!isValid) break;
        }
        
        this.setFieldError(field, isValid ? "" : errorMessage);
        return isValid;
    }
    
    validateForm() {
        let isFormValid = true;
        
        this.fields.forEach(field => {
            if (!this.validateField(field)) {
                isFormValid = false;
            }
        });
        
        return isFormValid;
    }
    
    setFieldError(field, errorMessage) {
        const errorElement = field.parentNode.querySelector(".error-message");
        
        if (errorMessage) {
            field.classList.add("error");
            if (errorElement) {
                errorElement.textContent = errorMessage;
            }
            this.errors.set(field.name, errorMessage);
        } else {
            field.classList.remove("error");
            if (errorElement) {
                errorElement.textContent = "";
            }
            this.errors.delete(field.name);
        }
    }
    
    showErrors() {
        const errorMessages = Array.from(this.errors.values());
        if (errorMessages.length > 0) {
            alert("请修正以下错误:\n" + errorMessages.join("\n"));
        }
    }
}

// 使用示例
// const validator = new FormValidator("myForm");

总结

JavaScript 事件的核心要点:

  1. 事件类型:鼠标事件、键盘事件、表单事件、窗口事件等
  2. 事件处理方式:addEventListener(推荐)、DOM 属性、内联处理器
  3. 事件对象:包含事件详细信息的对象
  4. 事件传播:捕获阶段、目标阶段、冒泡阶段
  5. 事件控制:preventDefault()、stopPropagation()
  6. 自定义事件:创建和触发自定义事件
  7. 事件委托:利用事件冒泡处理多个元素事件
  8. 性能优化:节流、防抖、事件监听器管理
  9. 最佳实践:及时清理、合理使用事件委托

掌握事件系统是创建交互式网页应用的基础。在下一章节中,我们将学习 JavaScript 的字符串。

本站内容仅供学习和研究使用。