JavaScript 教程

纯干货教学,从零开始学习 JavaScript

JavaScript 事件

JavaScript 事件是浏览器或用户与网页交互时发生的动作,如点击、鼠标移动、键盘输入等。事件处理是 JavaScript 与用户交互的核心,它允许我们创建响应式的、交互式的网页。

🎯 事件处理的重要性

事件处理是现代网页开发的基础,它使网页能够响应用户的操作,提供流畅的用户体验。掌握事件处理是成为一名优秀前端开发者的关键。

什么是事件?

事件是在网页中发生的动作,由用户或浏览器触发:

  • 用户事件:用户与网页交互时触发,如点击、鼠标移动、键盘输入等
  • 浏览器事件:浏览器自身触发的事件,如页面加载完成、窗口大小改变等
  • DOM 事件:DOM 元素触发的事件,如元素被点击、表单提交等

常见的事件类型

1. 鼠标事件

  • click:鼠标点击元素
  • dblclick:鼠标双击元素
  • mouseover:鼠标悬停在元素上
  • mouseout:鼠标离开元素
  • mousedown:鼠标按下元素
  • mouseup:鼠标释放元素
  • mousemove:鼠标在元素上移动
  • contextmenu:鼠标右键点击元素

2. 键盘事件

  • keydown:键盘按键被按下
  • keyup:键盘按键被释放
  • keypress:键盘按键被按下并释放(已弃用)
  • input:输入框内容改变
  • change:输入框内容改变并失去焦点

3. 表单事件

  • submit:表单提交
  • reset:表单重置
  • focus:元素获得焦点
  • blur:元素失去焦点
  • input:输入框内容改变
  • change:输入值改变

4. 窗口事件

  • load:页面所有资源加载完成
  • DOMContentLoaded:DOM 加载完成
  • resize:窗口大小改变
  • scroll:页面滚动
  • unload:页面卸载
  • beforeunload:页面即将卸载

5. 触摸事件

  • touchstart:触摸开始
  • touchmove:触摸移动
  • touchend:触摸结束
  • touchcancel:触摸被取消

事件处理方法

1. HTML 事件属性

直接在 HTML 元素上添加事件处理函数:

<!-- HTML 事件属性 -->
<button onclick="alert('Hello!')">Click Me</button>

<!-- 调用函数 -->
<script>
function handleClick() {
    alert('Button clicked!');
}
</script>
<button onclick="handleClick()">Click Me</button>

2. DOM 元素事件属性

通过 JavaScript 为 DOM 元素添加事件处理函数:

// HTML: <button id="myButton">Click Me</button>

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

// 使用 DOM 元素事件属性
button.onclick = function() {
    console.log("Button clicked!");
};

// 覆盖事件处理函数
button.onclick = function() {
    console.log("Button clicked again!");
};

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

3. addEventListener() 方法

使用 addEventListener() 方法添加事件监听器:

// HTML: <button id="myButton">Click Me</button>

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

// 添加点击事件监听器
button.addEventListener("click", function() {
    console.log("Button clicked!");
});

// 添加多个事件监听器
button.addEventListener("click", function() {
    console.log("Another click handler!");
});

// 使用命名函数
function handleClick() {
    console.log("Handled click!");
}

button.addEventListener("click", handleClick);

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

事件对象

当事件发生时,浏览器会创建一个事件对象,包含事件的详细信息:

// HTML: <button id="myButton">Click Me</button>

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

// 传递事件对象
button.addEventListener("click", function(event) {
    console.log("Event type:", event.type); // 事件类型
    console.log("Target element:", event.target); // 事件目标元素
    console.log("Current target:", event.currentTarget); // 当前处理事件的元素
    console.log("Client X:", event.clientX); // 鼠标 X 坐标
    console.log("Client Y:", event.clientY); // 鼠标 Y 坐标
    console.log("Page X:", event.pageX); // 相对于文档的 X 坐标
    console.log("Page Y:", event.pageY); // 相对于文档的 Y 坐标
    console.log("Event time:", event.timeStamp); // 事件时间戳
});

// 键盘事件对象
const input = document.getElementById("myInput");
input.addEventListener("keydown", function(event) {
    console.log("Key:", event.key); // 按键名称
    console.log("Key code:", event.keyCode); // 按键编码(已弃用)
    console.log("Code:", event.code); // 按键物理位置
    console.log("Shift:", event.shiftKey); // Shift 键是否按下
    console.log("Ctrl:", event.ctrlKey); // Ctrl 键是否按下
    console.log("Alt:", event.altKey); // Alt 键是否按下
});

事件冒泡和捕获

DOM 事件有三个阶段:

  1. 捕获阶段:事件从文档根节点向下传播到目标元素
  2. 目标阶段:事件到达目标元素
  3. 冒泡阶段:事件从目标元素向上传播回文档根节点
// HTML: <div id="outer">
//        <div id="inner">
//          <button id="btn">Click Me</button>
//        </div>
//      </div>

const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
const btn = document.getElementById("btn");

// 冒泡阶段(默认)
outer.addEventListener("click", function() {
    console.log("Outer div clicked (bubbling)");
});

inner.addEventListener("click", function() {
    console.log("Inner div clicked (bubbling)");
});

btn.addEventListener("click", function() {
    console.log("Button clicked (bubbling)");
});

// 捕获阶段
outer.addEventListener("click", function() {
    console.log("Outer div clicked (capturing)");
}, true); // 使用捕获阶段

inner.addEventListener("click", function() {
    console.log("Inner div clicked (capturing)");
}, true); // 使用捕获阶段

btn.addEventListener("click", function() {
    console.log("Button clicked (capturing)");
}, true); // 使用捕获阶段

事件委托

事件委托是一种将事件监听器添加到父元素,而不是每个子元素的技术,它利用了事件冒泡的特性:

// HTML: <ul id="myList">
//        <li>Item 1</li>
//        <li>Item 2</li>
//        <li>Item 3</li>
//        <li>Item 4</li>
//        <li>Item 5</li>
//      </ul>

const list = document.getElementById("myList");

// 使用事件委托
list.addEventListener("click", function(event) {
    // 检查目标元素是否是 li
    if (event.target.tagName === "LI") {
        console.log("List item clicked:", event.target.textContent);
        // 可以在这里添加更多逻辑
        event.target.style.backgroundColor = "lightblue";
        setTimeout(function() {
            event.target.style.backgroundColor = "";
        }, 1000);
    }
});

// 动态添加新的列表项
const newItem = document.createElement("li");
newItem.textContent = "Item 6";
list.appendChild(newItem);
// 新添加的列表项也会响应点击事件,因为事件委托

事件默认行为

许多事件都有默认行为,如链接点击跳转、表单提交刷新页面等。可以使用 preventDefault() 方法阻止默认行为:

// HTML: <a href="https://example.com" id="myLink">Example</a>

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

// 阻止链接的默认跳转行为
link.addEventListener("click", function(event) {
    event.preventDefault();
    console.log("Link clicked, but default behavior prevented");
    // 可以在这里添加自定义逻辑
});

// HTML: <form id="myForm">
//        <input type="text" name="username">
//        <button type="submit">Submit</button>
//      </form>

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

// 阻止表单的默认提交行为
form.addEventListener("submit", function(event) {
    event.preventDefault();
    console.log("Form submitted, but default behavior prevented");
    // 可以在这里添加表单验证和 AJAX 提交逻辑
});

// 阻止右键菜单
document.addEventListener("contextmenu", function(event) {
    event.preventDefault();
    console.log("Right-click prevented");
});

事件传播

可以使用 stopPropagation() 方法阻止事件继续传播(冒泡或捕获):

// HTML: <div id="outer">
//        <div id="inner">
//          <button id="btn">Click Me</button>
//        </div>
//      </div>

const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
const btn = document.getElementById("btn");

outer.addEventListener("click", function() {
    console.log("Outer div clicked");
});

inner.addEventListener("click", function() {
    console.log("Inner div clicked");
});

btn.addEventListener("click", function(event) {
    console.log("Button clicked");
    // 阻止事件冒泡
    event.stopPropagation();
});

// 点击按钮时,只会输出 "Button clicked"
// 事件不会冒泡到 inner 和 outer 元素

事件修饰符

在实际开发中,我们经常需要处理一些特殊情况,如:

// 处理键盘事件的特定按键
document.addEventListener("keydown", function(event) {
    if (event.key === "Enter") {
        console.log("Enter key pressed");
    } else if (event.key === "Escape") {
        console.log("Escape key pressed");
    } else if (event.key === "ArrowUp") {
        console.log("Up arrow pressed");
    } else if (event.key === "ArrowDown") {
        console.log("Down arrow pressed");
    }
});

// 处理组合键
document.addEventListener("keydown", function(event) {
    if (event.ctrlKey && event.key === "c") {
        console.log("Ctrl+C pressed");
    } else if (event.ctrlKey && event.key === "v") {
        console.log("Ctrl+V pressed");
    } else if (event.shiftKey && event.key === "Delete") {
        console.log("Shift+Delete pressed");
    }
});

// 处理鼠标按键
document.addEventListener("mousedown", function(event) {
    if (event.button === 0) {
        console.log("Left mouse button pressed");
    } else if (event.button === 1) {
        console.log("Middle mouse button pressed");
    } else if (event.button === 2) {
        console.log("Right mouse button pressed");
    }
});

事件监听的最佳实践

  • 使用 addEventListener() 方法:它允许添加多个事件监听器,并且可以控制事件阶段
  • 使用事件委托:对于大量相似元素,使用事件委托提高性能
  • 及时移除事件监听器:在不需要时移除事件监听器,避免内存泄漏
  • 使用命名函数:使用命名函数而不是匿名函数,便于后续移除
  • 合理使用事件默认行为:只在必要时阻止默认行为
  • 考虑事件冒泡和捕获:了解事件传播机制,合理使用
  • 优化事件处理函数:事件处理函数应该简洁高效,避免复杂计算
  • 使用 passive 事件监听器:对于滚动等事件,使用 passive 监听器提高性能
  • 处理触摸事件:考虑移动设备的触摸事件,提供良好的移动体验
  • 使用事件对象:充分利用事件对象提供的信息

事件处理常见问题

  • 问题 1: 事件监听器重复添加
  • 解决方案:避免在循环中重复添加事件监听器,使用事件委托或确保只添加一次监听器。

  • 问题 2: 事件监听器内存泄漏
  • 解决方案:在元素移除前,使用 removeEventListener() 移除事件监听器,避免内存泄漏。

  • 问题 3: 事件冒泡导致的意外行为
  • 解决方案:了解事件冒泡机制,在必要时使用 stopPropagation() 阻止事件传播。

  • 问题 4: 事件默认行为被意外阻止
  • 解决方案:只在必要时使用 preventDefault() 阻止默认行为,确保不会影响用户体验。

  • 问题 5: 移动设备触摸事件处理
  • 解决方案:同时考虑鼠标事件和触摸事件,使用触摸事件 API 提供更好的移动体验。

  • 问题 6: 事件处理函数中的 this 指向
  • 解决方案:了解事件处理函数中 this 的指向(默认指向目标元素),在必要时使用 bind() 或箭头函数。

  • 问题 7: 事件触发顺序
  • 解决方案:了解事件的触发顺序,特别是对于表单提交等复杂操作。

  • 问题 8: 性能问题
  • 解决方案:使用事件委托,避免添加过多事件监听器,优化事件处理函数的性能。

现代事件处理

现代前端开发中,我们经常使用框架如 React、Vue 或 Angular 来处理事件,但底层的事件处理原理仍然相同:

// React 中的事件处理
/*
function Button() {
    const handleClick = () => {
        console.log('Button clicked');
    };
    
    return (
        <button onClick={handleClick}>Click Me</button>
    );
}
*/

// Vue 中的事件处理
/*
<template>
    <button @click="handleClick">Click Me</button>
</template>

<script>
export default {
    methods: {
        handleClick() {
            console.log('Button clicked');
        }
    }
};
</script>
*/

// Angular 中的事件处理
/*
<button (click)="handleClick()">Click Me</button>

// TypeScript 代码
@Component({
    selector: 'app-button',
    templateUrl: './button.component.html'
})
export class ButtonComponent {
    handleClick() {
        console.log('Button clicked');
    }
}
*/

📝 学习检查

通过本章节的学习,你应该了解:

  • 事件的基本概念和常见类型
  • 不同的事件处理方法(HTML 属性、DOM 属性、addEventListener)
  • 事件对象的使用
  • 事件的三个阶段(捕获、目标、冒泡)
  • 事件委托的原理和应用
  • 如何阻止默认行为和事件传播
  • 事件处理的最佳实践
  • 事件处理中的常见问题和解决方案