Appearance
事件对象与参数传递
学习目标
通过本章学习,你将掌握:
- React事件对象的结构和属性
- 如何正确使用事件对象
- 向事件处理器传递参数的方法
- 事件对象的持久化问题
- 常用事件对象属性详解
- React 19中的事件对象特性
- 实战案例和最佳实践
第一部分:React事件对象
1.1 事件对象基础
jsx
function EventObjectBasics() {
const handleClick = (e) => {
// e是React的合成事件对象
console.log('事件类型:', e.type); // 'click'
console.log('目标元素:', e.target); // DOM元素
console.log('当前元素:', e.currentTarget); // 绑定事件的元素
console.log('时间戳:', e.timeStamp); // 事件发生时间
console.log('是否冒泡:', e.bubbles); // true/false
};
return <button onClick={handleClick}>点击查看事件对象</button>;
}事件对象的属性
jsx
function EventProperties() {
const handleEvent = (e) => {
// 通用属性
console.log('type:', e.type); // 事件类型
console.log('target:', e.target); // 触发事件的元素
console.log('currentTarget:', e.currentTarget); // 绑定事件的元素
console.log('timeStamp:', e.timeStamp); // 时间戳
console.log('bubbles:', e.bubbles); // 是否冒泡
console.log('cancelable:', e.cancelable); // 是否可取消
console.log('defaultPrevented:', e.defaultPrevented); // 是否已阻止默认
console.log('isTrusted:', e.isTrusted); // 是否由用户触发
// 鼠标事件特有属性
console.log('clientX:', e.clientX); // 相对视口X
console.log('clientY:', e.clientY); // 相对视口Y
console.log('pageX:', e.pageX); // 相对页面X
console.log('pageY:', e.pageY); // 相对页面Y
console.log('screenX:', e.screenX); // 相对屏幕X
console.log('screenY:', e.screenY); // 相对屏幕Y
console.log('button:', e.button); // 鼠标按键
console.log('buttons:', e.buttons); // 按下的按键
console.log('altKey:', e.altKey); // Alt键
console.log('ctrlKey:', e.ctrlKey); // Ctrl键
console.log('shiftKey:', e.shiftKey); // Shift键
console.log('metaKey:', e.metaKey); // Meta键
// 键盘事件特有属性
console.log('key:', e.key); // 按键值
console.log('code:', e.code); // 按键码
console.log('keyCode:', e.keyCode); // 键码(已废弃)
};
return (
<div>
<button onClick={handleEvent}>鼠标事件</button>
<input onKeyDown={handleEvent} />
</div>
);
}1.2 target vs currentTarget
jsx
function TargetVsCurrentTarget() {
const handleClick = (e) => {
console.log('target:', e.target.tagName); // 实际点击的元素
console.log('currentTarget:', e.currentTarget.tagName); // 绑定事件的元素
};
return (
<div onClick={handleClick}> {/* currentTarget: DIV */}
<p>段落文本</p> {/* 点击这里,target: P */}
<button>按钮</button> {/* 点击这里,target: BUTTON */}
<span>文本</span> {/* 点击这里,target: SPAN */}
</div>
);
}
// 实际应用:点击外部关闭
function ClickOutside() {
const [isOpen, setIsOpen] = useState(false);
const menuRef = useRef(null);
useEffect(() => {
const handleClickOutside = (e) => {
// 点击不在菜单内,关闭菜单
if (menuRef.current && !menuRef.current.contains(e.target)) {
setIsOpen(false);
}
};
if (isOpen) {
document.addEventListener('click', handleClickOutside);
}
return () => {
document.removeEventListener('click', handleClickOutside);
};
}, [isOpen]);
return (
<div>
<button onClick={() => setIsOpen(true)}>打开菜单</button>
{isOpen && (
<div ref={menuRef} className="menu">
<div>选项1</div>
<div>选项2</div>
</div>
)}
</div>
);
}1.3 阻止默认行为
jsx
function PreventDefaultExamples() {
// 1. 阻止链接跳转
const handleLinkClick = (e) => {
e.preventDefault();
console.log('链接被点击,但不跳转');
};
// 2. 阻止表单提交
const handleSubmit = (e) => {
e.preventDefault();
console.log('表单提交被拦截');
};
// 3. 阻止右键菜单
const handleContextMenu = (e) => {
e.preventDefault();
console.log('右键菜单被禁用');
};
// 4. 阻止拖拽默认行为
const handleDragOver = (e) => {
e.preventDefault(); // 允许drop
};
const handleDrop = (e) => {
e.preventDefault(); // 阻止浏览器打开文件
console.log('放下文件');
};
return (
<div>
<a href="https://example.com" onClick={handleLinkClick}>
点击不跳转的链接
</a>
<form onSubmit={handleSubmit}>
<button type="submit">提交</button>
</form>
<div onContextMenu={handleContextMenu}>
右键点击这里
</div>
<div
onDragOver={handleDragOver}
onDrop={handleDrop}
style={{ border: '1px solid black', padding: 20 }}
>
拖拽文件到这里
</div>
</div>
);
}第二部分:传递参数的方法
2.1 使用箭头函数传参
jsx
function ArrowFunctionParams() {
const [selectedId, setSelectedId] = useState(null);
const items = [
{ id: 1, name: '项目A' },
{ id: 2, name: '项目B' },
{ id: 3, name: '项目C' }
];
// 方式1:箭头函数传参
const handleClick = (id, name) => {
console.log('点击了:', id, name);
setSelectedId(id);
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{/* 使用箭头函数传递参数 */}
<button onClick={() => handleClick(item.id, item.name)}>
{item.name}
</button>
</li>
))}
</ul>
);
}
// 同时接收事件对象和参数
function WithEventObject() {
const handleClick = (id, e) => {
console.log('ID:', id);
console.log('事件:', e.type);
console.log('目标:', e.target);
};
return (
<button onClick={(e) => handleClick(123, e)}>
点击
</button>
);
}2.2 使用bind传参
jsx
function BindParams() {
const items = [
{ id: 1, name: '项目A' },
{ id: 2, name: '项目B' }
];
const handleClick = (id, name, e) => {
console.log('ID:', id);
console.log('Name:', name);
console.log('Event:', e.type);
};
return (
<ul>
{items.map(item => (
<li key={item.id}>
{/* bind传参:事件对象在最后 */}
<button onClick={handleClick.bind(null, item.id, item.name)}>
{item.name}
</button>
</li>
))}
</ul>
);
}
// 类组件中使用bind
class BindInClass extends React.Component {
handleClick(id, name, e) {
console.log(this.state); // this正确绑定
console.log('ID:', id);
}
render() {
return (
<button onClick={this.handleClick.bind(this, 1, 'Item')}>
点击
</button>
);
}
}2.3 使用data属性传参
jsx
function DataAttributes() {
const handleClick = (e) => {
const id = e.target.dataset.id;
const name = e.target.dataset.name;
console.log('ID:', id, 'Name:', name);
};
const items = [
{ id: 1, name: '项目A' },
{ id: 2, name: '项目B' }
];
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button
data-id={item.id}
data-name={item.name}
onClick={handleClick}
>
{item.name}
</button>
</li>
))}
</ul>
);
}
// 事件委托与data属性
function EventDelegation() {
const handleListClick = (e) => {
const button = e.target.closest('button');
if (button && button.dataset.id) {
const id = button.dataset.id;
console.log('点击项:', id);
}
};
return (
<ul onClick={handleListClick}>
{items.map(item => (
<li key={item.id}>
<button data-id={item.id}>{item.name}</button>
</li>
))}
</ul>
);
}2.4 使用闭包传参
jsx
function ClosureParams() {
const createClickHandler = (id, name) => {
return (e) => {
console.log('ID:', id);
console.log('Name:', name);
console.log('Event:', e.type);
};
};
const items = [
{ id: 1, name: '项目A' },
{ id: 2, name: '项目B' }
];
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={createClickHandler(item.id, item.name)}>
{item.name}
</button>
</li>
))}
</ul>
);
}
// 优化:使用useCallback避免重复创建
function OptimizedClosure() {
const [items] = useState([
{ id: 1, name: '项目A' },
{ id: 2, name: '项目B' }
]);
const createClickHandler = useCallback((id, name) => {
return (e) => {
console.log('ID:', id, 'Name:', name);
};
}, []);
return (
<ul>
{items.map(item => (
<li key={item.id}>
<button onClick={createClickHandler(item.id, item.name)}>
{item.name}
</button>
</li>
))}
</ul>
);
}第三部分:不同参数传递方式对比
3.1 性能对比
jsx
// 方式1:箭头函数(最常用)
function Method1() {
return items.map(item => (
<button key={item.id} onClick={() => handleClick(item.id)}>
{item.name}
</button>
));
}
// 优点:简洁,可读性好
// 缺点:每次渲染创建新函数
// 方式2:bind
function Method2() {
return items.map(item => (
<button key={item.id} onClick={handleClick.bind(null, item.id)}>
{item.name}
</button>
));
}
// 优点:语法标准
// 缺点:每次渲染创建新函数,可读性稍差
// 方式3:data属性
function Method3() {
const handleClick = (e) => {
const id = e.target.dataset.id;
handleItemClick(id);
};
return items.map(item => (
<button key={item.id} data-id={item.id} onClick={handleClick}>
{item.name}
</button>
));
}
// 优点:只有一个事件处理器
// 缺点:参数只能是字符串,需要转换
// 方式4:闭包
function Method4() {
const createHandler = (id) => (e) => {
handleClick(id, e);
};
return items.map(item => (
<button key={item.id} onClick={createHandler(item.id)}>
{item.name}
</button>
));
}
// 优点:灵活
// 缺点:每次渲染创建新函数
// 性能敏感场景:使用方式3(data属性 + 事件委托)
// 一般场景:使用方式1(箭头函数,简洁)3.2 复杂参数传递
jsx
function ComplexParams() {
const [selected, setSelected] = useState(null);
const items = [
{ id: 1, name: '项目A', category: '工作', priority: 'high' },
{ id: 2, name: '项目B', category: '生活', priority: 'low' }
];
// 传递整个对象
const handleSelectItem = (item) => {
setSelected(item);
console.log('选中:', item);
};
// 传递多个参数
const handleUpdate = (id, field, value) => {
console.log(`更新ID ${id}的${field}为${value}`);
};
return (
<div>
<ul>
{items.map(item => (
<li key={item.id}>
{/* 传递整个对象 */}
<button onClick={() => handleSelectItem(item)}>
选择 {item.name}
</button>
{/* 传递多个参数 */}
<button onClick={() => handleUpdate(item.id, 'priority', 'high')}>
设为高优先级
</button>
</li>
))}
</ul>
{selected && (
<div>
<h3>已选择: {selected.name}</h3>
<p>分类: {selected.category}</p>
<p>优先级: {selected.priority}</p>
</div>
)}
</div>
);
}3.3 事件对象与参数组合
jsx
function CombineEventAndParams() {
// 参数在前,事件对象在后
const handleClick = (id, name, e) => {
console.log('ID:', id);
console.log('Name:', name);
console.log('Clicked element:', e.target);
console.log('Shift pressed:', e.shiftKey);
};
return (
<div>
{/* 箭头函数 */}
<button onClick={(e) => handleClick(1, 'Item A', e)}>
方式1
</button>
{/* bind */}
<button onClick={handleClick.bind(null, 2, 'Item B')}>
方式2
</button>
</div>
);
}
// 实际应用:多选功能
function MultiSelect() {
const [selected, setSelected] = useState([]);
const handleItemClick = (id, e) => {
if (e.ctrlKey || e.metaKey) {
// Ctrl/Cmd多选
setSelected(prev =>
prev.includes(id)
? prev.filter(i => i !== id)
: [...prev, id]
);
} else if (e.shiftKey) {
// Shift范围选择
console.log('范围选择');
} else {
// 普通单选
setSelected([id]);
}
};
const items = [
{ id: 1, name: '项目1' },
{ id: 2, name: '项目2' },
{ id: 3, name: '项目3' }
];
return (
<ul>
{items.map(item => (
<li
key={item.id}
onClick={(e) => handleItemClick(item.id, e)}
style={{
backgroundColor: selected.includes(item.id) ? 'lightblue' : 'white'
}}
>
{item.name}
</li>
))}
</ul>
);
}第四部分:键盘事件详解
4.1 键盘事件属性
jsx
function KeyboardEventDetails() {
const handleKeyDown = (e) => {
console.log('键值 key:', e.key); // 'a', 'Enter', 'ArrowUp'
console.log('键码 code:', e.code); // 'KeyA', 'Enter', 'ArrowUp'
console.log('键码 keyCode:', e.keyCode); // 65, 13, 38(已废弃)
console.log('Alt:', e.altKey);
console.log('Ctrl:', e.ctrlKey);
console.log('Shift:', e.shiftKey);
console.log('Meta:', e.metaKey);
console.log('Repeat:', e.repeat); // 是否长按
};
return <input onKeyDown={handleKeyDown} />;
}常用按键判断
jsx
function KeyDetection() {
const handleKeyDown = (e) => {
// 回车键
if (e.key === 'Enter') {
console.log('回车');
}
// Escape键
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 保存');
}
if (e.ctrlKey && e.key === 'z') {
e.preventDefault();
console.log('Ctrl+Z 撤销');
}
if (e.shiftKey && e.key === 'Enter') {
console.log('Shift+Enter 换行');
}
// 字母键
if (e.key >= 'a' && e.key <= 'z') {
console.log('字母键:', e.key);
}
// 数字键
if (e.key >= '0' && e.key <= '9') {
console.log('数字键:', e.key);
}
};
return <input onKeyDown={handleKeyDown} />;
}快捷键实现
jsx
function ShortcutKeys() {
const [content, setContent] = useState('');
const [history, setHistory] = useState([]);
const handleKeyDown = (e) => {
// Ctrl+S 保存
if ((e.ctrlKey || e.metaKey) && e.key === 's') {
e.preventDefault();
save(content);
}
// Ctrl+Z 撤销
if ((e.ctrlKey || e.metaKey) && e.key === 'z') {
e.preventDefault();
undo();
}
// Ctrl+Shift+Z 重做
if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key === 'z') {
e.preventDefault();
redo();
}
// Esc 关闭
if (e.key === 'Escape') {
close();
}
};
const save = (content) => {
console.log('保存:', content);
};
const undo = () => {
console.log('撤销');
};
const redo = () => {
console.log('重做');
};
const close = () => {
console.log('关闭');
};
return (
<textarea
value={content}
onChange={e => setContent(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="尝试快捷键: Ctrl+S, Ctrl+Z, Esc"
/>
);
}第五部分:鼠标事件详解
5.1 鼠标位置
jsx
function MousePosition() {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({
clientX: e.clientX, // 相对于视口
clientY: e.clientY,
pageX: e.pageX, // 相对于页面
pageY: e.pageY,
screenX: e.screenX, // 相对于屏幕
screenY: e.screenY,
offsetX: e.nativeEvent.offsetX, // 相对于元素
offsetY: e.nativeEvent.offsetY
});
};
return (
<div
onMouseMove={handleMouseMove}
style={{ height: 300, border: '1px solid black' }}
>
<pre>{JSON.stringify(position, null, 2)}</pre>
</div>
);
}鼠标按键判断
jsx
function MouseButtons() {
const handleMouseDown = (e) => {
// button属性
switch(e.button) {
case 0:
console.log('左键');
break;
case 1:
console.log('中键/滚轮');
break;
case 2:
console.log('右键');
break;
case 3:
console.log('侧键1');
break;
case 4:
console.log('侧键2');
break;
}
// buttons属性(多个按键)
if (e.buttons === 1) console.log('左键按下');
if (e.buttons === 2) console.log('右键按下');
if (e.buttons === 3) console.log('左右键同时按下');
};
return (
<div
onMouseDown={handleMouseDown}
onContextMenu={e => e.preventDefault()}
>
点击这里测试鼠标按键
</div>
);
}5.2 拖拽事件
jsx
function DragAndDrop() {
const [dragging, setDragging] = useState(null);
const handleDragStart = (e, item) => {
setDragging(item);
e.dataTransfer.effectAllowed = 'move';
e.dataTransfer.setData('text/plain', item.id);
};
const handleDragOver = (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'move';
};
const handleDrop = (e, targetCategory) => {
e.preventDefault();
const itemId = e.dataTransfer.getData('text/plain');
console.log(`移动项${itemId}到${targetCategory}`);
setDragging(null);
};
const handleDragEnd = () => {
setDragging(null);
};
const items = [
{ id: 1, name: '项目1', category: 'todo' },
{ id: 2, name: '项目2', category: 'todo' }
];
return (
<div>
<div>
<h3>待办</h3>
{items.filter(i => i.category === 'todo').map(item => (
<div
key={item.id}
draggable
onDragStart={(e) => handleDragStart(e, item)}
onDragEnd={handleDragEnd}
>
{item.name}
</div>
))}
</div>
<div
onDragOver={handleDragOver}
onDrop={(e) => handleDrop(e, 'done')}
>
<h3>完成</h3>
{items.filter(i => i.category === 'done').map(item => (
<div key={item.id}>{item.name}</div>
))}
</div>
</div>
);
}第六部分:表单事件详解
6.1 输入事件
jsx
function InputEvents() {
const [value, setValue] = useState('');
const handleChange = (e) => {
console.log('onChange:', e.target.value);
setValue(e.target.value);
};
const handleInput = (e) => {
console.log('onInput:', e.target.value);
};
const handleFocus = (e) => {
console.log('获得焦点');
e.target.select(); // 自动全选
};
const handleBlur = (e) => {
console.log('失去焦点');
};
return (
<input
value={value}
onChange={handleChange}
onInput={handleInput}
onFocus={handleFocus}
onBlur={handleBlur}
/>
);
}6.2 表单提交事件
jsx
function FormSubmitEvent() {
const handleSubmit = (e) => {
e.preventDefault(); // 阻止默认提交
// 方式1:通过e.target获取表单数据
const form = e.target;
const username = form.elements.username.value;
const password = form.elements.password.value;
console.log({ username, password });
};
// 方式2:使用FormData API
const handleSubmitFormData = (e) => {
e.preventDefault();
const formData = new FormData(e.target);
const data = Object.fromEntries(formData);
console.log(data);
};
// 方式3:受控组件(推荐)
const [formData, setFormData] = useState({
username: '',
password: ''
});
const handleSubmitControlled = (e) => {
e.preventDefault();
console.log(formData);
};
return (
<form onSubmit={handleSubmitControlled}>
<input
name="username"
value={formData.username}
onChange={e => setFormData({
...formData,
username: e.target.value
})}
/>
<input
name="password"
type="password"
value={formData.password}
onChange={e => setFormData({
...formData,
password: e.target.value
})}
/>
<button type="submit">提交</button>
</form>
);
}第七部分:React 19新特性
7.1 Server Actions事件处理
jsx
'use server';
async function handleSubmit(formData) {
const data = {
name: formData.get('name'),
email: formData.get('email')
};
await db.users.create(data);
revalidatePath('/users');
return { success: true, message: '创建成功' };
}
// Client Component
'use client';
import { useActionState } from 'react';
function ServerActionForm() {
const [state, formAction, isPending] = useActionState(handleSubmit, null);
return (
<form action={formAction}>
<input name="name" required />
<input name="email" type="email" required />
<button type="submit" disabled={isPending}>
{isPending ? '提交中...' : '提交'}
</button>
{state?.message && <p>{state.message}</p>}
</form>
);
}7.2 useFormStatus
jsx
'use client';
import { useFormStatus } from 'react-dom';
function SubmitButton() {
const { pending, data, method, action } = useFormStatus();
return (
<button type="submit" disabled={pending}>
{pending ? '提交中...' : '提交'}
</button>
);
}
function MyForm() {
async function handleSubmit(formData) {
'use server';
await saveData(formData);
}
return (
<form action={handleSubmit}>
<input name="username" />
<SubmitButton />
</form>
);
}第八部分:实战案例
8.1 图片裁剪器
jsx
function ImageCropper() {
const [cropping, setCropping] = useState(false);
const [cropArea, setCropArea] = useState({
startX: 0,
startY: 0,
endX: 0,
endY: 0
});
const handleMouseDown = (e) => {
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setCropping(true);
setCropArea({
startX: x,
startY: y,
endX: x,
endY: y
});
};
const handleMouseMove = (e) => {
if (!cropping) return;
const rect = e.currentTarget.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
setCropArea(prev => ({
...prev,
endX: x,
endY: y
}));
};
const handleMouseUp = () => {
setCropping(false);
console.log('裁剪区域:', cropArea);
};
return (
<div
onMouseDown={handleMouseDown}
onMouseMove={handleMouseMove}
onMouseUp={handleMouseUp}
style={{
position: 'relative',
width: 400,
height: 300,
border: '1px solid black',
cursor: 'crosshair'
}}
>
<img src="/image.jpg" alt="裁剪" />
{cropping && (
<div
style={{
position: 'absolute',
left: Math.min(cropArea.startX, cropArea.endX),
top: Math.min(cropArea.startY, cropArea.endY),
width: Math.abs(cropArea.endX - cropArea.startX),
height: Math.abs(cropArea.endY - cropArea.startY),
border: '2px dashed blue'
}}
/>
)}
</div>
);
}8.2 键盘导航
jsx
function KeyboardNavigation() {
const [items] = useState(['项目1', '项目2', '项目3', '项目4', '项目5']);
const [selectedIndex, setSelectedIndex] = useState(0);
const handleKeyDown = (e) => {
switch(e.key) {
case 'ArrowDown':
e.preventDefault();
setSelectedIndex(prev =>
prev < items.length - 1 ? prev + 1 : prev
);
break;
case 'ArrowUp':
e.preventDefault();
setSelectedIndex(prev => prev > 0 ? prev - 1 : prev);
break;
case 'Enter':
console.log('选中:', items[selectedIndex]);
break;
case 'Home':
e.preventDefault();
setSelectedIndex(0);
break;
case 'End':
e.preventDefault();
setSelectedIndex(items.length - 1);
break;
}
};
return (
<div
tabIndex={0}
onKeyDown={handleKeyDown}
style={{ outline: 'none' }}
>
<ul>
{items.map((item, index) => (
<li
key={index}
style={{
backgroundColor: index === selectedIndex ? 'lightblue' : 'white'
}}
>
{item}
</li>
))}
</ul>
<p>使用方向键、Home、End导航,Enter选择</p>
</div>
);
}练习题
基础练习
- 创建一个按钮,点击时打印事件对象的各个属性
- 实现一个输入框,显示按下的键名
- 创建一个列表,点击项时传递该项的ID
进阶练习
- 实现一个支持快捷键的文本编辑器
- 创建一个可拖拽排序的列表
- 实现一个支持Ctrl多选的列表
高级练习
- 创建一个图片裁剪工具
- 实现一个支持键盘导航的下拉菜单
- 使用React 19 Server Actions处理表单提交
通过本章学习,你已经掌握了React事件对象的使用和参数传递技巧。这些知识是构建交互式应用的基础。继续学习,成为事件处理专家!