Skip to content

事件对象与参数传递

学习目标

通过本章学习,你将掌握:

  • 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>
  );
}

练习题

基础练习

  1. 创建一个按钮,点击时打印事件对象的各个属性
  2. 实现一个输入框,显示按下的键名
  3. 创建一个列表,点击项时传递该项的ID

进阶练习

  1. 实现一个支持快捷键的文本编辑器
  2. 创建一个可拖拽排序的列表
  3. 实现一个支持Ctrl多选的列表

高级练习

  1. 创建一个图片裁剪工具
  2. 实现一个支持键盘导航的下拉菜单
  3. 使用React 19 Server Actions处理表单提交

通过本章学习,你已经掌握了React事件对象的使用和参数传递技巧。这些知识是构建交互式应用的基础。继续学习,成为事件处理专家!