← 返回目录 | 下一课:列表渲染与Key →

第5课: 事件处理

用户交互与响应详解

🎯 学习目标

1. React事件系统概述

React事件系统:React使用合成事件(SyntheticEvent)系统,它封装了原生浏览器事件,提供了一致的API,并自动处理跨浏览器兼容性问题。

React事件的特点

传统事件 vs React事件

传统DOM事件

// 传统方式
const button = document.getElementById('btn');
button.onclick = function(event) {
    console.log('按钮被点击');
    event.preventDefault();
};

React事件

// React方式
function Button() {
    const handleClick = (event) => {
        console.log('按钮被点击');
        event.preventDefault();
    };
    
    return (
        <button onClick={handleClick}>
            点击我
        </button>
    );
}

2. 基本事件处理

2.1 事件绑定语法

// 事件绑定的基本语法
function EventExample() {
    // 定义事件处理函数
    const handleClick = () => {
        console.log('按钮被点击了!');
    };
    
    const handleMouseOver = () => {
        console.log('鼠标悬停');
    };
    
    const handleMouseOut = () => {
        console.log('鼠标离开');
    };
    
    return (
        <div>
            <button onClick={handleClick}>
                点击我
            </button>
            <div 
                onMouseOver={handleMouseOver}
                onMouseOut={handleMouseOut}
            >
                鼠标悬停区域
            </div>
        </div>
    );
}

重要警告:事件处理函数应该传递函数引用,而不是调用函数!

2.2 常见错误示例

❌ 错误方式

// 错误:立即调用函数
function BadExample() {
    const handleClick = () => {
        console.log('点击');
    };
    
    return (
        <button onClick={handleClick()}>
            点击我
        </button>
    );
}

✅ 正确方式

// 正确:传递函数引用
function GoodExample() {
    const handleClick = () => {
        console.log('点击');
    };
    
    return (
        <button onClick={handleClick}>
            点击我
        </button>
    );
}

3. 事件对象

事件对象:React事件处理函数会自动接收一个事件对象参数,这个对象包含了事件的相关信息。

3.1 事件对象的常用属性

function EventObjectExample() {
    const handleClick = (event) => {
        console.log('事件类型:', event.type);
        console.log('目标元素:', event.target);
        console.log('当前元素:', event.currentTarget);
        console.log('鼠标位置:', event.clientX, event.clientY);
        console.log('按键:', event.key);
    };
    
    const handleInputChange = (event) => {
        console.log('输入值:', event.target.value);
        console.log('元素名称:', event.target.name);
    };
    
    return (
        <div>
            <button onClick={handleClick}>
                点击查看事件对象
            </button>
            <input 
                type="text"
                name="username"
                onChange={handleInputChange}
                placeholder="输入文字"
            />
        </div>
    );
}

3.2 阻止默认行为

function PreventDefaultExample() {
    const handleSubmit = (event) => {
        event.preventDefault();
        console.log('表单提交被阻止');
    };
    
    const handleLinkClick = (event) => {
        event.preventDefault();
        console.log('链接点击被阻止');
    };
    
    return (
        <div>
            <form onSubmit={handleSubmit}>
                <button type="submit">
                    提交表单(不会刷新页面)
                </button>
            </form>
            
            <a href="https://example.com" onClick={handleLinkClick}>
                点击链接(不会跳转)
            </a>
        </div>
    );
}

3.3 阻止事件冒泡

function StopPropagationExample() {
    const handleParentClick = () => {
        console.log('父元素被点击');
    };
    
    const handleChildClick = (event) => {
        event.stopPropagation();
        console.log('子元素被点击(阻止冒泡)');
    };
    
    return (
        <div 
            onClick={handleParentClick}
            style={{ 
                padding: '20px', 
                backgroundColor: '#e0e0e0',
                cursor: 'pointer'
            }}
        >
            父元素
            <div 
                onClick={handleChildClick}
                style={{ 
                    padding: '10px', 
                    backgroundColor: '#61DAFB',
                    marginTop: '10px',
                    cursor: 'pointer'
                }}
            >
                子元素(点击不会触发父元素)
            </div>
        </div>
    );
}

4. 常用事件类型

事件类型 React属性 说明
鼠标事件 onClick, onDoubleClick, onMouseDown, onMouseUp, onMouseMove, onMouseOver, onMouseOut 鼠标点击、移动等操作
键盘事件 onKeyDown, onKeyPress, onKeyUp 键盘按键操作
表单事件 onChange, onSubmit, onInput, onFocus, onBlur 表单输入和提交
焦点事件 onFocus, onBlur 元素获得或失去焦点
剪贴板事件 onCopy, onCut, onPaste 复制、剪切、粘贴操作
滚动事件 onScroll 元素滚动

5. 传递参数到事件处理函数

5.1 使用箭头函数

function PassParametersExample() {
    const handleClick = (message, id) => {
        console.log('消息:', message);
        console.log('ID:', id);
    };
    
    const items = [
        { id: 1, name: '项目1' },
        { id: 2, name: '项目2' },
        { id: 3, name: '项目3' }
    ];
    
    return (
        <div>
            <button onClick={() => handleClick('Hello', 1)}>
                点击我
            </button>
            
            <ul>
                {items.map(item => (
                    <li key={item.id}>
                        {item.name}
                        <button onClick={() => handleClick('删除', item.id)}>
                            删除
                        </button>
                    </li>
                ))}
            </ul>
        </div>
    );
}

5.2 使用bind方法

function BindExample() {
    const handleClick = function(id, event) {
        console.log('ID:', id);
        console.log('事件:', event);
    };
    
    return (
        <button onClick={handleClick.bind(null, 1)}>
            点击我
        </button>
    );
}

推荐方式:使用箭头函数比bind更简洁易读,是React中传递参数的推荐方式。

6. 键盘事件处理

function KeyboardEventExample() {
    const handleKeyDown = (event) => {
        console.log('按下的键:', event.key);
        console.log('键码:', event.keyCode);
        
        if (event.key === 'Enter') {
            console.log('按下了回车键');
        }
        
        if (event.key === 'Escape') {
            console.log('按下了ESC键');
        }
    };
    
    const handleKeyPress = (event) => {
        console.log('按键字符:', event.key);
    };
    
    const handleKeyUp = (event) => {
        console.log('释放的键:', event.key);
    };
    
    return (
        <div>
            <input 
                type="text"
                placeholder="在此输入(按Enter键)"
                onKeyDown={handleKeyDown}
                onKeyPress={handleKeyPress}
                onKeyUp={handleKeyUp}
            />
        </div>
    );
}

7. 表单事件处理

7.1 输入框事件

function InputEventExample() {
    const [value, setValue] = useState('');
    
    const handleChange = (event) => {
        setValue(event.target.value);
        console.log('当前值:', event.target.value);
    };
    
    const handleFocus = () => {
        console.log('输入框获得焦点');
    };
    
    const handleBlur = () => {
        console.log('输入框失去焦点');
    };
    
    return (
        <div>
            <input 
                type="text"
                value={value}
                onChange={handleChange}
                onFocus={handleFocus}
                onBlur={handleBlur}
                placeholder="输入文字"
            />
            <p>输入的值: {value}</p>
        </div>
    );
}

7.2 表单提交

function FormSubmitExample() {
    const [formData, setFormData] = useState({
        username: '',
        email: '',
        password: ''
    });
    
    const handleChange = (event) => {
        const { name, value } = event.target;
        setFormData({
            ...formData,
            [name]: value
        });
    };
    
    const handleSubmit = (event) => {
        event.preventDefault();
        console.log('表单数据:', formData);
        
        // 这里可以发送数据到服务器
        // fetch('/api/register', {
        //     method: 'POST',
        //     body: JSON.stringify(formData)
        // });
    };
    
    return (
        <form onSubmit={handleSubmit}>
            <div>
                <label>用户名:</label>
                <input 
                    type="text"
                    name="username"
                    value={formData.username}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label>邮箱:</label>
                <input 
                    type="email"
                    name="email"
                    value={formData.email}
                    onChange={handleChange}
                />
            </div>
            
            <div>
                <label>密码:</label>
                <input 
                    type="password"
                    name="password"
                    value={formData.password}
                    onChange={handleChange}
                />
            </div>
            
            <button type="submit">提交</button>
        </form>
    );
}

8. 自定义事件处理

8.1 通过props传递事件处理函数

// 子组件
function ChildButton({ onClick, children }) {
    return (
        <button onClick={onClick}>
            {children}
        </button>
    );
}

// 父组件
function ParentComponent() {
    const handleClick = () => {
        console.log('按钮被点击了!');
    };
    
    return (
        <div>
            <ChildButton onClick={handleClick}>
                点击我
            </ChildButton>
        </div>
    );
}

8.2 自定义事件处理器

function CustomEventHandler() {
    const handleMouseMove = (event) => {
        const x = event.clientX;
        const y = event.clientY;
        console.log(`鼠标位置: (${x}, ${y})`);
    };
    
    return (
        <div 
            onMouseMove={handleMouseMove}
            style={{
                width: '300px',
                height: '200px',
                backgroundColor: '#f0f0f0',
                display: 'flex',
                alignItems: 'center',
                justifyContent: 'center',
                cursor: 'crosshair'
            }}
        >
            在这里移动鼠标
        </div>
    );
}

9. 事件处理的最佳实践

最佳实践建议

import { useState, useCallback } from 'react';

function BestPracticesExample() {
    const [count, setCount] = useState(0);
    
    // 使用useCallback优化性能
    const handleClick = useCallback(() => {
        setCount(prevCount => prevCount + 1);
    }, []);
    
    const handleReset = useCallback(() => {
        setCount(0);
    }, []);
    
    return (
        <div>
            <h2>计数: {count}</h2>
            <button onClick={handleClick}>增加</button>
            <button onClick={handleReset}>重置</button>
        </div>
    );
}

💡 动手练习

任务:创建一个交互式计算器

要求:

// 参考答案
function Calculator() {
    const [display, setDisplay] = useState('0');
    const [history, setHistory] = useState([]);
    const [operator, setOperator] = useState(null);
    const [previousValue, setPreviousValue] = useState(null);
    const [waitingForOperand, setWaitingForOperand] = useState(false);
    
    const inputDigit = (digit) => {
        if (waitingForOperand) {
            setDisplay(String(digit));
            setWaitingForOperand(false);
        } else {
            setDisplay(display === '0' ? String(digit) : display + digit);
        }
    };
    
    const inputDot = () => {
        if (waitingForOperand) {
            setDisplay('0.');
            setWaitingForOperand(false);
        } else if (display.indexOf('.') === -1) {
            setDisplay(display + '.');
        }
    };
    
    const clear = () => {
        setDisplay('0');
        setOperator(null);
        setPreviousValue(null);
        setWaitingForOperand(false);
    };
    
    const performOperation = (nextOperator) => {
        const inputValue = parseFloat(display);
        
        if (previousValue === null) {
            setPreviousValue(inputValue);
        } else if (operator) {
            const currentValue = previousValue || 0;
            const newValue = calculate(currentValue, inputValue, operator);
            
            setDisplay(String(newValue));
            setPreviousValue(newValue);
            setHistory([...history, `${currentValue} ${operator} ${inputValue} = ${newValue}`]);
        }
        
        setWaitingForOperand(true);
        setOperator(nextOperator);
    };
    
    const calculate = (firstValue, secondValue, operation) => {
        switch (operation) {
            case '+':
                return firstValue + secondValue;
            case '-':
                return firstValue - secondValue;
            case '×':
                return firstValue * secondValue;
            case '÷':
                return secondValue !== 0 ? firstValue / secondValue : 'Error';
            default:
                return secondValue;
        }
    };
    
    const handleEqual = () => {
        if (!operator || previousValue === null) return;
        
        const inputValue = parseFloat(display);
        const result = calculate(previousValue, inputValue, operator);
        
        setDisplay(String(result));
        setHistory([...history, `${previousValue} ${operator} ${inputValue} = ${result}`]);
        setPreviousValue(null);
        setOperator(null);
        setWaitingForOperand(true);
    };
    
    const handleKeyDown = (event) => {
        const key = event.key;
        
        if (/[0-9]/.test(key)) {
            inputDigit(parseInt(key));
        } else if (key === '.') {
            inputDot();
        } else if (key === '+') {
            performOperation('+');
        } else if (key === '-') {
            performOperation('-');
        } else if (key === '*') {
            performOperation('×');
        } else if (key === '/') {
            event.preventDefault();
            performOperation('÷');
        } else if (key === 'Enter' || key === '=') {
            handleEqual();
        } else if (key === 'Escape' || key === 'c' || key === 'C') {
            clear();
        } else if (key === 'Backspace') {
            setDisplay(display.length > 1 ? display.slice(0, -1) : '0');
        }
    };
    
    return (
        <div style={{ maxWidth: '300px', margin: '20px auto' }}>
            <div style={{ 
                backgroundColor: '#20232a', 
                color: '#61DAFB', 
                padding: '20px', 
                fontSize: '32px', 
                textAlign: 'right',
                marginBottom: '10px'
            }}>
                {display}
            </div>
            
            <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: '5px' }}>
                <button onClick={clear}>C</button>
                <button onClick={() => performOperation('÷')}>÷</button>
                <button onClick={() => performOperation('×')}>×</button>
                <button onClick={() => setDisplay(display.length > 1 ? display.slice(0, -1) : '0')}>←</button>
                
                {[7, 8, 9].map(digit => (
                    <button key={digit} onClick={() => inputDigit(digit)}>{digit}</button>
                ))}
                <button onClick={() => performOperation('-')}>-</button>
                
                {[4, 5, 6].map(digit => (
                    <button key={digit} onClick={() => inputDigit(digit)}>{digit}</button>
                ))}
                <button onClick={() => performOperation('+')}>+</button>
                
                {[1, 2, 3].map(digit => (
                    <button key={digit} onClick={() => inputDigit(digit)}>{digit}</button>
                ))}
                <button 
                    onClick={handleEqual} 
                    style={{ gridRow: 'span 2' }}
                >
                    =
                </button>
                
                <button 
                    onClick={() => inputDigit(0)} 
                    style={{ gridColumn: 'span 2' }}
                >
                    0
                </button>
                <button onClick={inputDot}>.</button>
            </div>
            
            <div style={{ marginTop: '20px' }}>
                <h3>计算历史</h3>
                <ul style={{ maxHeight: '150px', overflowY: 'auto' }}>
                    {history.map((item, index) => (
                        <li key={index}>{item}</li>
                    ))}
                </ul>
            </div>
        </div>
    );
}

📝 关键概念总结