Skip to content

事件处理

事件是用户与网页交互时发生的动作,如点击、输入、滚动等。通过事件处理,JavaScript 可以响应用户的操作。本章将详细介绍事件绑定、事件对象、事件委托等内容。

事件绑定

行内事件(不推荐)

html
<!-- 直接在 HTML 中绑定事件(不推荐) -->
<button onclick="alert('点击了')">点击</button>

<button onclick="handleClick()">点击</button>

<script>
    function handleClick() {
        console.log('按钮被点击');
    }
</script>

DOM 属性绑定

javascript
let btn = document.getElementById('btn');

// 将函数赋值给事件属性
btn.onclick = function() {
    console.log('按钮被点击');
};

// 使用箭头函数
btn.onclick = () => {
    console.log('按钮被点击');
};

// 缺点:只能绑定一个处理函数
btn.onclick = function() {
    console.log('第一个处理函数');
};
btn.onclick = function() {
    console.log('第二个处理函数');  // 只有这个会执行
};

// 移除事件
btn.onclick = null;

addEventListener(推荐)

javascript
let btn = document.getElementById('btn');

// addEventListener(事件类型, 处理函数, 选项)
btn.addEventListener('click', function() {
    console.log('按钮被点击');
});

// 可以绑定多个处理函数
btn.addEventListener('click', function() {
    console.log('第一个处理函数');
});
btn.addEventListener('click', function() {
    console.log('第二个处理函数');  // 两个都会执行
});

// 使用命名函数(便于移除)
function handleClick() {
    console.log('点击了');
}
btn.addEventListener('click', handleClick);

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

// 使用箭头函数
btn.addEventListener('click', () => {
    console.log('箭头函数处理');
});

// 注意:箭头函数无法直接移除(因为是匿名函数)
// 需要保存引用
const handler = () => console.log('点击');
btn.addEventListener('click', handler);
btn.removeEventListener('click', handler);

// 第三个参数:选项对象
btn.addEventListener('click', handleClick, {
    capture: false,  // 是否在捕获阶段触发
    once: true,      // 是否只触发一次
    passive: true    // 是否不会调用 preventDefault
});

// 简写:第三个参数为布尔值表示 capture
btn.addEventListener('click', handleClick, true);  // 捕获阶段

事件对象

javascript
document.getElementById('btn').addEventListener('click', function(event) {
    // event 是事件对象,包含事件的详细信息
    
    // 事件类型
    console.log(event.type);  // 'click'
    
    // 触发事件的元素
    console.log(event.target);  // 实际点击的元素
    
    // 绑定事件监听器的元素
    console.log(event.currentTarget);  // 当前元素
    
    // 事件发生的时间戳
    console.log(event.timeStamp);
    
    // 鼠标位置
    console.log(event.clientX, event.clientY);  // 相对于视口
    console.log(event.pageX, event.pageY);      // 相对于文档
    console.log(event.screenX, event.screenY);  // 相对于屏幕
    
    // 鼠标按钮
    console.log(event.button);  // 0=左键, 1=中键, 2=右键
    
    // 修饰键
    console.log(event.ctrlKey);   // Ctrl 是否按下
    console.log(event.shiftKey);  // Shift 是否按下
    console.log(event.altKey);    // Alt 是否按下
    console.log(event.metaKey);   // Meta 键(Mac 的 Command)
});

阻止默认行为

javascript
// 阻止链接跳转
document.querySelector('a').addEventListener('click', function(e) {
    e.preventDefault();  // 阻止默认行为
    console.log('链接被点击,但不会跳转');
});

// 阻止表单提交
document.querySelector('form').addEventListener('submit', function(e) {
    e.preventDefault();
    console.log('表单未提交');
});

// 阻止右键菜单
document.addEventListener('contextmenu', function(e) {
    e.preventDefault();
    console.log('右键菜单被阻止');
});

// 检查是否调用了 preventDefault
console.log(e.defaultPrevented);  // true

阻止事件冒泡

javascript
// 事件冒泡:事件从目标元素向上传播
document.getElementById('child').addEventListener('click', function(e) {
    console.log('子元素被点击');
    e.stopPropagation();  // 阻止事件继续传播
});

document.getElementById('parent').addEventListener('click', function(e) {
    console.log('父元素被点击');  // 不会执行
});

// stopImmediatePropagation:阻止后续处理函数
btn.addEventListener('click', function(e) {
    console.log('第一个处理函数');
    e.stopImmediatePropagation();
});

btn.addEventListener('click', function(e) {
    console.log('第二个处理函数');  // 不会执行
});

常用事件类型

鼠标事件

javascript
let box = document.getElementById('box');

// 点击事件
box.addEventListener('click', function(e) {
    console.log('单击');
});

box.addEventListener('dblclick', function(e) {
    console.log('双击');
});

// 鼠标按下/抬起
box.addEventListener('mousedown', function(e) {
    console.log('鼠标按下');
});

box.addEventListener('mouseup', function(e) {
    console.log('鼠标抬起');
});

// 鼠标移入/移出
box.addEventListener('mouseenter', function(e) {
    console.log('鼠标进入(不冒泡)');
});

box.addEventListener('mouseleave', function(e) {
    console.log('鼠标离开(不冒泡)');
});

// 鼠标移动(冒泡)
box.addEventListener('mouseover', function(e) {
    console.log('鼠标悬停');
});

box.addEventListener('mouseout', function(e) {
    console.log('鼠标移出');
});

// 鼠标移动
box.addEventListener('mousemove', function(e) {
    console.log('鼠标位置:', e.clientX, e.clientY);
});

// 区别:
// mouseenter/mouseleave:不冒泡,只在元素边界触发
// mouseover/mouseout:冒泡,子元素也会触发

键盘事件

javascript
let input = document.getElementById('input');

// 键盘按下
input.addEventListener('keydown', function(e) {
    console.log('按下键:', e.key);
    console.log('键码:', e.code);
    console.log('ASCII 码:', e.keyCode);  // 已废弃
    console.log('Ctrl 键:', e.ctrlKey);
    console.log('Shift 键:', e.shiftKey);
    
    // 阻止某些按键
    if (e.key === 'Enter') {
        e.preventDefault();
        console.log('回车键被阻止');
    }
});

// 键盘抬起
input.addEventListener('keyup', function(e) {
    console.log('松开键:', e.key);
});

// 字符输入(只对可打印字符有效)
input.addEventListener('keypress', function(e) {
    console.log('输入字符:', e.key);  // 已废弃
});

// 常用按键检测
document.addEventListener('keydown', function(e) {
    // 回车键
    if (e.key === 'Enter') {
        console.log('按下回车');
    }
    
    // ESC 键
    if (e.key === 'Escape') {
        console.log('按下 ESC');
    }
    
    // 方向键
    if (e.key === 'ArrowUp') console.log('上');
    if (e.key === 'ArrowDown') console.log('下');
    if (e.key === 'ArrowLeft') console.log('左');
    if (e.key === 'ArrowRight') console.log('右');
    
    // 快捷键
    if (e.ctrlKey && e.key === 's') {
        e.preventDefault();
        console.log('Ctrl+S 保存');
    }
});

表单事件

javascript
let form = document.querySelector('form');
let input = document.querySelector('input');

// 输入事件(实时触发)
input.addEventListener('input', function(e) {
    console.log('输入内容:', e.target.value);
});

// 值变化(失焦时触发)
input.addEventListener('change', function(e) {
    console.log('值已改变:', e.target.value);
});

// 获得焦点
input.addEventListener('focus', function(e) {
    console.log('获得焦点');
    e.target.style.borderColor = 'blue';
});

// 失去焦点
input.addEventListener('blur', function(e) {
    console.log('失去焦点');
    e.target.style.borderColor = '';
});

// 表单提交
form.addEventListener('submit', function(e) {
    e.preventDefault();
    console.log('表单提交');
    let formData = new FormData(form);
    console.log('数据:', Object.fromEntries(formData));
});

// 表单重置
form.addEventListener('reset', function(e) {
    console.log('表单重置');
});

// 选择文本
input.addEventListener('select', function(e) {
    console.log('选中了文本');
    console.log('选中范围:', e.target.selectionStart, e.target.selectionEnd);
});

// 复选框/单选框
let checkbox = document.querySelector('input[type="checkbox"]');
checkbox.addEventListener('change', function(e) {
    console.log('选中状态:', e.target.checked);
});

// 下拉框
let select = document.querySelector('select');
select.addEventListener('change', function(e) {
    console.log('选中值:', e.target.value);
});

文档/窗口事件

javascript
// DOM 加载完成
document.addEventListener('DOMContentLoaded', function() {
    console.log('DOM 加载完成');
});

// 页面加载完成(包括图片等资源)
window.addEventListener('load', function() {
    console.log('页面加载完成');
});

// 页面卸载前
window.addEventListener('beforeunload', function(e) {
    e.preventDefault();
    e.returnValue = '';  // 显示确认对话框
});

// 页面卸载
window.addEventListener('unload', function() {
    console.log('页面卸载');
});

// 滚动事件
window.addEventListener('scroll', function() {
    console.log('滚动位置:', window.scrollY);
});

// 窗口大小改变
window.addEventListener('resize', function() {
    console.log('窗口大小:', window.innerWidth, window.innerHeight);
});

// 哈希改变
window.addEventListener('hashchange', function() {
    console.log('哈希:', location.hash);
});

// 历史记录改变(pushState/replaceState 不会触发)
window.addEventListener('popstate', function(e) {
    console.log('历史记录改变');
});

事件委托

什么是事件委托

javascript
// 事件委托:利用事件冒泡,在父元素上统一处理子元素的事件

// 不使用事件委托(需要为每个元素绑定事件)
let items = document.querySelectorAll('li');
items.forEach(item => {
    item.addEventListener('click', function() {
        console.log('点击了:', this.textContent);
    });
});

// 使用事件委托(只需在父元素绑定一次)
let list = document.getElementById('list');
list.addEventListener('click', function(e) {
    // 检查点击的是否是 li 元素
    if (e.target.tagName === 'LI') {
        console.log('点击了:', e.target.textContent);
    }
});

// 使用 matches() 方法
list.addEventListener('click', function(e) {
    if (e.target.matches('li.item')) {
        console.log('点击了 item');
    }
});

// 使用 closest() 方法(处理嵌套元素)
list.addEventListener('click', function(e) {
    let li = e.target.closest('li');
    if (li && list.contains(li)) {
        console.log('点击了:', li.textContent);
    }
});

事件委托的优势

javascript
// 1. 减少事件绑定数量,提高性能
// 不需要为每个子元素绑定事件

// 2. 动态添加的元素也能响应事件
let list = document.getElementById('list');
list.addEventListener('click', function(e) {
    if (e.target.tagName === 'LI') {
        console.log('点击了:', e.target.textContent);
    }
});

// 动态添加的元素也能响应
let newLi = document.createElement('li');
newLi.textContent = '新项目';
list.appendChild(newLi);  // 点击也能触发事件

// 3. 方便管理事件
// 所有事件处理逻辑集中在一处

事件委托示例

html
<ul id="menu">
    <li data-action="home">首页</li>
    <li data-action="about">关于</li>
    <li data-action="contact">联系</li>
</ul>

<script>
let menu = document.getElementById('menu');
menu.addEventListener('click', function(e) {
    let item = e.target.closest('li');
    if (!item) return;
    
    let action = item.dataset.action;
    switch (action) {
        case 'home':
            console.log('显示首页');
            break;
        case 'about':
            console.log('显示关于');
            break;
        case 'contact':
            console.log('显示联系');
            break;
    }
});
</script>

自定义事件

创建和触发自定义事件

javascript
// 创建自定义事件
let event = new CustomEvent('myEvent', {
    detail: { name: '张三', age: 25 },  // 自定义数据
    bubbles: true,      // 是否冒泡
    cancelable: true    // 是否可以取消
});

// 监听自定义事件
document.addEventListener('myEvent', function(e) {
    console.log('自定义事件触发了');
    console.log('数据:', e.detail);
});

// 触发事件
document.dispatchEvent(event);

// 使用 Event 构造函数(简单事件)
let simpleEvent = new Event('simpleEvent');
document.addEventListener('simpleEvent', function() {
    console.log('简单事件');
});
document.dispatchEvent(simpleEvent);

自定义事件示例

javascript
// 创建一个简单的计数器组件
class Counter {
    constructor(element) {
        this.element = element;
        this.count = 0;
        this.render();
        this.bindEvents();
    }
    
    render() {
        this.element.innerHTML = `
            <button class="decrease">-</button>
            <span class="count">${this.count}</span>
            <button class="increase">+</button>
        `;
    }
    
    bindEvents() {
        this.element.addEventListener('click', (e) => {
            if (e.target.matches('.increase')) {
                this.count++;
                this.update();
            } else if (e.target.matches('.decrease')) {
                this.count--;
                this.update();
            }
        });
    }
    
    update() {
        this.element.querySelector('.count').textContent = this.count;
        
        // 触发自定义事件
        let event = new CustomEvent('countChange', {
            detail: { count: this.count },
            bubbles: true
        });
        this.element.dispatchEvent(event);
    }
}

// 使用
let counter = new Counter(document.getElementById('counter'));

// 监听计数变化
document.addEventListener('countChange', function(e) {
    console.log('计数变为:', e.detail.count);
});

事件循环与异步

javascript
// JavaScript 是单线程的,使用事件循环处理异步操作

// 宏任务(Macro Task)
setTimeout(() => console.log('setTimeout'), 0);
setInterval(() => console.log('setInterval'), 1000);

// 微任务(Micro Task)
Promise.resolve().then(() => console.log('Promise'));

// 执行顺序
console.log('同步代码');

setTimeout(() => console.log('宏任务'), 0);

Promise.resolve().then(() => console.log('微任务'));

console.log('同步代码结束');

// 输出顺序:
// 1. 同步代码
// 2. 同步代码结束
// 3. 微任务
// 4. 宏任务

// 事件循环:同步代码 -> 微任务 -> 宏任务 -> 渲染 -> 微任务 -> ...

实战案例

拖拽功能

javascript
let dragElement = document.getElementById('draggable');
let isDragging = false;
let offsetX, offsetY;

dragElement.addEventListener('mousedown', function(e) {
    isDragging = true;
    offsetX = e.clientX - dragElement.offsetLeft;
    offsetY = e.clientY - dragElement.offsetTop;
    dragElement.style.cursor = 'grabbing';
});

document.addEventListener('mousemove', function(e) {
    if (!isDragging) return;
    
    dragElement.style.left = (e.clientX - offsetX) + 'px';
    dragElement.style.top = (e.clientY - offsetY) + 'px';
});

document.addEventListener('mouseup', function() {
    isDragging = false;
    dragElement.style.cursor = 'grab';
});

无限滚动

javascript
let container = document.getElementById('container');
let loading = false;

window.addEventListener('scroll', function() {
    // 检查是否滚动到底部
    let scrollTop = window.scrollY;
    let windowHeight = window.innerHeight;
    let documentHeight = document.documentElement.scrollHeight;
    
    if (scrollTop + windowHeight >= documentHeight - 100 && !loading) {
        loadMore();
    }
});

function loadMore() {
    loading = true;
    console.log('加载更多...');
    
    // 模拟异步加载
    setTimeout(() => {
        for (let i = 0; i < 10; i++) {
            let item = document.createElement('div');
            item.textContent = '新内容 ' + Date.now();
            container.appendChild(item);
        }
        loading = false;
    }, 1000);
}

键盘导航

javascript
let items = document.querySelectorAll('.nav-item');
let currentIndex = 0;

// 高亮当前项
function highlight(index) {
    items.forEach((item, i) => {
        item.classList.toggle('active', i === index);
    });
}

// 点击选择
items.forEach((item, index) => {
    item.addEventListener('click', () => {
        currentIndex = index;
        highlight(index);
    });
});

// 键盘导航
document.addEventListener('keydown', function(e) {
    if (e.key === 'ArrowDown' || e.key === 'ArrowRight') {
        currentIndex = (currentIndex + 1) % items.length;
        highlight(currentIndex);
    } else if (e.key === 'ArrowUp' || e.key === 'ArrowLeft') {
        currentIndex = (currentIndex - 1 + items.length) % items.length;
        highlight(currentIndex);
    } else if (e.key === 'Enter') {
        items[currentIndex].click();
    }
});

小结

本章学习了 JavaScript 事件处理的核心知识:

  • 事件绑定:行内事件、DOM 属性绑定、addEventListener
  • 事件对象:事件类型、目标元素、鼠标位置、修饰键
  • 阻止行为:preventDefault、stopPropagation
  • 常用事件:鼠标事件、键盘事件、表单事件、文档事件
  • 事件委托:利用冒泡机制统一处理事件
  • 自定义事件:创建和触发自定义事件
  • 事件循环:宏任务、微任务的执行顺序

下一章我们将学习 BOM与定时器,了解浏览器对象模型和定时任务。