Skip to content

事件绑定基础

学习目标

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

  • React事件处理的基本概念
  • 事件绑定的多种方式
  • 事件处理器的命名规范
  • this绑定的各种方法
  • 事件处理的最佳实践
  • React 19中的事件处理新特性
  • 常见问题与解决方案

第一部分:React事件基础

1.1 什么是React事件

React事件是对原生DOM事件的跨浏览器包装,提供了一致的API和更好的性能。

基本语法

jsx
// HTML原生事件(小写)
<button onclick="handleClick()">Click</button>

// React事件(驼峰命名)
<button onClick={handleClick}>Click</button>

// 主要区别:
// 1. 事件名使用驼峰命名法(onClick而非onclick)
// 2. 事件处理器是函数引用,不是字符串
// 3. 不能通过返回false阻止默认行为

简单示例

jsx
function Button() {
  // 定义事件处理器
  const handleClick = () => {
    console.log('按钮被点击了');
  };
  
  return (
    <button onClick={handleClick}>
      点击我
    </button>
  );
}

// 函数组件的完整示例
function Counter() {
  const [count, setCount] = useState(0);
  
  const increment = () => {
    setCount(count + 1);
  };
  
  const decrement = () => {
    setCount(count - 1);
  };
  
  const reset = () => {
    setCount(0);
  };
  
  return (
    <div>
      <p>计数:{count}</p>
      <button onClick={increment}>+1</button>
      <button onClick={decrement}>-1</button>
      <button onClick={reset}>重置</button>
    </div>
  );
}

1.2 常用事件类型

鼠标事件

jsx
function MouseEvents() {
  const handleClick = () => console.log('click');
  const handleDoubleClick = () => console.log('double click');
  const handleMouseDown = () => console.log('mouse down');
  const handleMouseUp = () => console.log('mouse up');
  const handleMouseEnter = () => console.log('mouse enter');
  const handleMouseLeave = () => console.log('mouse leave');
  const handleMouseMove = () => console.log('mouse move');
  const handleMouseOver = () => console.log('mouse over');
  const handleMouseOut = () => console.log('mouse out');
  const handleContextMenu = (e) => {
    e.preventDefault();
    console.log('context menu');
  };
  
  return (
    <div
      onClick={handleClick}
      onDoubleClick={handleDoubleClick}
      onMouseDown={handleMouseDown}
      onMouseUp={handleMouseUp}
      onMouseEnter={handleMouseEnter}
      onMouseLeave={handleMouseLeave}
      onMouseMove={handleMouseMove}
      onMouseOver={handleMouseOver}
      onMouseOut={handleMouseOut}
      onContextMenu={handleContextMenu}
    >
      鼠标事件测试区域
    </div>
  );
}

键盘事件

jsx
function KeyboardEvents() {
  const handleKeyDown = (e) => {
    console.log('Key down:', e.key);
  };
  
  const handleKeyUp = (e) => {
    console.log('Key up:', e.key);
  };
  
  const handleKeyPress = (e) => {
    console.log('Key press:', e.key);
  };
  
  return (
    <input
      type="text"
      onKeyDown={handleKeyDown}
      onKeyUp={handleKeyUp}
      onKeyPress={handleKeyPress}
      placeholder="输入测试"
    />
  );
}

// 实际应用:回车提交
function SearchBox() {
  const [query, setQuery] = useState('');
  
  const handleKeyDown = (e) => {
    if (e.key === 'Enter') {
      handleSearch();
    }
  };
  
  const handleSearch = () => {
    console.log('搜索:', query);
  };
  
  return (
    <div>
      <input
        type="text"
        value={query}
        onChange={e => setQuery(e.target.value)}
        onKeyDown={handleKeyDown}
        placeholder="按回车搜索"
      />
      <button onClick={handleSearch}>搜索</button>
    </div>
  );
}

表单事件

jsx
function FormEvents() {
  const handleSubmit = (e) => {
    e.preventDefault();
    console.log('表单提交');
  };
  
  const handleChange = (e) => {
    console.log('值改变:', e.target.value);
  };
  
  const handleFocus = () => {
    console.log('获得焦点');
  };
  
  const handleBlur = () => {
    console.log('失去焦点');
  };
  
  const handleInput = (e) => {
    console.log('输入:', e.target.value);
  };
  
  const handleReset = () => {
    console.log('表单重置');
  };
  
  return (
    <form onSubmit={handleSubmit} onReset={handleReset}>
      <input
        type="text"
        onChange={handleChange}
        onFocus={handleFocus}
        onBlur={handleBlur}
        onInput={handleInput}
      />
      <button type="submit">提交</button>
      <button type="reset">重置</button>
    </form>
  );
}

其他常用事件

jsx
function OtherEvents() {
  // 滚动事件
  const handleScroll = () => {
    console.log('滚动');
  };
  
  // 拖拽事件
  const handleDragStart = () => console.log('开始拖拽');
  const handleDrag = () => console.log('拖拽中');
  const handleDragEnd = () => console.log('拖拽结束');
  const handleDrop = (e) => {
    e.preventDefault();
    console.log('放下');
  };
  const handleDragOver = (e) => {
    e.preventDefault();
  };
  
  // 焦点事件
  const handleFocus = () => console.log('焦点进入');
  const handleBlur = () => console.log('焦点离开');
  
  // 剪贴板事件
  const handleCopy = () => console.log('复制');
  const handleCut = () => console.log('剪切');
  const handlePaste = () => console.log('粘贴');
  
  return (
    <div>
      <div 
        onScroll={handleScroll}
        style={{ height: 100, overflow: 'auto' }}
      >
        <div style={{ height: 200 }}>滚动区域</div>
      </div>
      
      <div
        draggable
        onDragStart={handleDragStart}
        onDrag={handleDrag}
        onDragEnd={handleDragEnd}
      >
        可拖拽元素
      </div>
      
      <div
        onDrop={handleDrop}
        onDragOver={handleDragOver}
      >
        放置区域
      </div>
      
      <input
        onFocus={handleFocus}
        onBlur={handleBlur}
        onCopy={handleCopy}
        onCut={handleCut}
        onPaste={handlePaste}
      />
    </div>
  );
}

第二部分:事件绑定方式

2.1 函数组件中的事件绑定

方式1:直接定义函数

jsx
function DirectFunction() {
  const handleClick = () => {
    console.log('点击');
  };
  
  return <button onClick={handleClick}>点击</button>;
}

// 带参数的函数
function WithParams() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>增加</button>
    </div>
  );
}

方式2:内联箭头函数

jsx
function InlineArrow() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <p>{count}</p>
      {/* 简单逻辑可以内联 */}
      <button onClick={() => setCount(count + 1)}>增加</button>
      
      {/* 复杂逻辑不推荐内联 */}
      <button onClick={() => {
        console.log('当前值:', count);
        setCount(count + 1);
        console.log('新值:', count + 1);
      }}>
        增加并打印
      </button>
    </div>
  );
}

// 注意:内联函数的性能影响
function PerformanceIssue() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      {/* 每次渲染都创建新函数 */}
      <ExpensiveChild onClick={() => setCount(count + 1)} />
      
      {/* 如果ExpensiveChild使用React.memo,会失效 */}
    </div>
  );
}

方式3:使用useCallback

jsx
import { useState, useCallback } from 'react';

function UseCallbackExample() {
  const [count, setCount] = useState(0);
  
  // 缓存事件处理器
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);  // 空依赖,函数引用永不变
  
  return (
    <div>
      <p>{count}</p>
      <ExpensiveChild onClick={handleClick} />
    </div>
  );
}

// 带依赖的useCallback
function WithDependency({ initialValue }) {
  const [count, setCount] = useState(0);
  const [multiplier, setMultiplier] = useState(initialValue);
  
  const handleClick = useCallback(() => {
    setCount(c => c + multiplier);
  }, [multiplier]);  // multiplier变化时重新创建
  
  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>增加 {multiplier}</button>
      <button onClick={() => setMultiplier(m => m * 2)}>
        翻倍倍数
      </button>
    </div>
  );
}

2.2 类组件中的事件绑定

方式1:构造函数中绑定

jsx
class BindInConstructor extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    
    // 在构造函数中绑定this
    this.handleClick = this.handleClick.bind(this);
  }
  
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }
  
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.handleClick}>增加</button>
      </div>
    );
  }
}

方式2:类字段箭头函数(推荐)

jsx
class ArrowInClass extends React.Component {
  state = { count: 0 };
  
  // 类字段 + 箭头函数(自动绑定this)
  handleClick = () => {
    this.setState({ count: this.state.count + 1 });
  };
  
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.handleClick}>增加</button>
      </div>
    );
  }
}

方式3:render中绑定(不推荐)

jsx
class BindInRender extends React.Component {
  state = { count: 0 };
  
  handleClick() {
    this.setState({ count: this.state.count + 1 });
  }
  
  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        
        {/* 方法1:bind(每次渲染创建新函数) */}
        <button onClick={this.handleClick.bind(this)}>
          Bind
        </button>
        
        {/* 方法2:箭头函数(每次渲染创建新函数) */}
        <button onClick={() => this.handleClick()}>
          Arrow
        </button>
      </div>
    );
  }
}

// 性能问题
const MemoChild = React.memo(({ onClick }) => {
  console.log('子组件渲染');
  return <button onClick={onClick}>子按钮</button>;
});

class Parent extends React.Component {
  handleClick() {
    console.log('点击');
  }
  
  render() {
    return (
      <div>
        {/* 每次都创建新函数,导致MemoChild重新渲染 */}
        <MemoChild onClick={() => this.handleClick()} />
      </div>
    );
  }
}

2.3 this绑定问题详解

为什么需要绑定this

jsx
class ThisProblem extends React.Component {
  state = { message: 'Hello' };
  
  handleClick() {
    // 错误:this是undefined
    console.log(this.state.message);
    // TypeError: Cannot read property 'state' of undefined
  }
  
  render() {
    return <button onClick={this.handleClick}>点击</button>;
  }
}

// 原因:
// JavaScript中,类方法不会自动绑定this
// 事件处理器作为回调传递时,丢失了this上下文

// 模拟过程:
const obj = {
  name: 'Alice',
  greet() {
    console.log(this.name);
  }
};

obj.greet();  // 'Alice'(this指向obj)

const greet = obj.greet;
greet();  // undefined(this丢失)

所有绑定方案对比

jsx
class AllBindingMethods extends React.Component {
  state = { count: 0 };
  
  // 方案1:构造函数绑定
  constructor(props) {
    super(props);
    this.method1 = this.method1.bind(this);
  }
  
  method1() {
    console.log('方案1', this.state.count);
  }
  
  // 方案2:类字段箭头函数(推荐)
  method2 = () => {
    console.log('方案2', this.state.count);
  };
  
  // 方案3:普通方法
  method3() {
    console.log('方案3', this.state.count);
  }
  
  render() {
    return (
      <div>
        <button onClick={this.method1}>方案1</button>
        <button onClick={this.method2}>方案2</button>
        
        {/* 方案3的使用方式 */}
        <button onClick={this.method3.bind(this)}>
          方案3a(不推荐)
        </button>
        <button onClick={() => this.method3()}>
          方案3b(不推荐)
        </button>
      </div>
    );
  }
}

// 对比总结:
/*
方案1(构造函数绑定):
- 优点:性能好,只绑定一次
- 缺点:代码冗长,需要手动绑定

方案2(类字段箭头函数):
- 优点:语法简洁,自动绑定
- 缺点:每个实例都有自己的函数副本

方案3a/3b(render中绑定):
- 优点:无
- 缺点:性能差,每次渲染都创建新函数

推荐:方案2(类字段箭头函数)
*/

第三部分:事件处理器命名规范

3.1 标准命名约定

jsx
function NamingConvention() {
  // 事件处理器命名:handle + 事件名
  const handleClick = () => {};
  const handleChange = () => {};
  const handleSubmit = () => {};
  const handleMouseEnter = () => {};
  const handleKeyDown = () => {};
  
  // 回调props命名:on + 事件名
  const onClick = () => {};
  const onChange = () => {};
  const onSubmit = () => {};
  
  // 具体业务命名
  const handleLoginClick = () => {};
  const handleUserNameChange = () => {};
  const handleFormSubmit = () => {};
  const handleDeleteButtonClick = () => {};
  
  return (
    <div>
      <button onClick={handleClick}>通用点击</button>
      <button onClick={handleLoginClick}>登录点击</button>
      <input onChange={handleChange} />
      <input onChange={handleUserNameChange} />
    </div>
  );
}

3.2 组件间传递事件

jsx
// 父组件
function Parent() {
  const handleChildClick = (data) => {
    console.log('子组件点击:', data);
  };
  
  return (
    <Child onButtonClick={handleChildClick} />
  );
}

// 子组件
function Child({ onButtonClick }) {
  const handleClick = () => {
    const data = { id: 1, name: 'Item' };
    onButtonClick(data);  // 调用父组件传来的函数
  };
  
  return <button onClick={handleClick}>点击我</button>;
}

// 完整示例:Todo应用
function TodoApp() {
  const [todos, setTodos] = useState([]);
  
  const handleAddTodo = (text) => {
    setTodos([...todos, { id: Date.now(), text }]);
  };
  
  const handleDeleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };
  
  const handleToggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id
        ? { ...todo, completed: !todo.completed }
        : todo
    ));
  };
  
  return (
    <div>
      <TodoInput onAdd={handleAddTodo} />
      <TodoList
        todos={todos}
        onDelete={handleDeleteTodo}
        onToggle={handleToggleTodo}
      />
    </div>
  );
}

function TodoInput({ onAdd }) {
  const [text, setText] = useState('');
  
  const handleSubmit = (e) => {
    e.preventDefault();
    if (text.trim()) {
      onAdd(text);
      setText('');
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={text}
        onChange={e => setText(e.target.value)}
      />
      <button type="submit">添加</button>
    </form>
  );
}

function TodoList({ todos, onDelete, onToggle }) {
  return (
    <ul>
      {todos.map(todo => (
        <TodoItem
          key={todo.id}
          todo={todo}
          onDelete={() => onDelete(todo.id)}
          onToggle={() => onToggle(todo.id)}
        />
      ))}
    </ul>
  );
}

function TodoItem({ todo, onDelete, onToggle }) {
  return (
    <li>
      <input
        type="checkbox"
        checked={todo.completed}
        onChange={onToggle}
      />
      <span>{todo.text}</span>
      <button onClick={onDelete}>删除</button>
    </li>
  );
}

第四部分:阻止默认行为和冒泡

4.1 阻止默认行为

jsx
function PreventDefault() {
  // 方式1:使用preventDefault
  const handleSubmit = (e) => {
    e.preventDefault();  // 阻止表单提交
    console.log('表单不会提交到服务器');
  };
  
  const handleLinkClick = (e) => {
    e.preventDefault();  // 阻止链接跳转
    console.log('不会跳转');
  };
  
  const handleContextMenu = (e) => {
    e.preventDefault();  // 阻止右键菜单
    console.log('右键菜单被禁用');
  };
  
  // React中不能返回false阻止默认行为(不同于HTML)
  const handleClickWrong = () => {
    return false;  // 无效!
  };
  
  return (
    <div>
      <form onSubmit={handleSubmit}>
        <button type="submit">提交</button>
      </form>
      
      <a href="https://example.com" onClick={handleLinkClick}>
        链接(点击不跳转)
      </a>
      
      <div onContextMenu={handleContextMenu}>
        右键点击这里
      </div>
    </div>
  );
}

// 实际应用:自定义表单提交
function CustomForm() {
  const [formData, setFormData] = useState({
    username: '',
    password: ''
  });
  
  const handleSubmit = async (e) => {
    e.preventDefault();  // 阻止默认提交
    
    try {
      const response = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify(formData)
      });
      
      const data = await response.json();
      console.log('登录成功:', data);
    } catch (error) {
      console.error('登录失败:', error);
    }
  };
  
  return (
    <form onSubmit={handleSubmit}>
      <input
        value={formData.username}
        onChange={e => setFormData({
          ...formData,
          username: e.target.value
        })}
      />
      <input
        type="password"
        value={formData.password}
        onChange={e => setFormData({
          ...formData,
          password: e.target.value
        })}
      />
      <button type="submit">登录</button>
    </form>
  );
}

4.2 阻止事件冒泡

jsx
function StopPropagation() {
  const handleParentClick = () => {
    console.log('父元素点击');
  };
  
  const handleChildClick = (e) => {
    e.stopPropagation();  // 阻止冒泡
    console.log('子元素点击');
    // 父元素的handleParentClick不会执行
  };
  
  return (
    <div onClick={handleParentClick}>
      <p>父元素(点击会触发)</p>
      <button onClick={handleChildClick}>
        子元素(点击不会冒泡到父元素)
      </button>
    </div>
  );
}

// 实际应用:模态框
function Modal({ onClose, children }) {
  const handleBackdropClick = () => {
    onClose();  // 点击背景关闭
  };
  
  const handleModalClick = (e) => {
    e.stopPropagation();  // 阻止冒泡到背景
  };
  
  return (
    <div className="backdrop" onClick={handleBackdropClick}>
      <div className="modal" onClick={handleModalClick}>
        {children}
        <button onClick={onClose}>关闭</button>
      </div>
    </div>
  );
}

// 下拉菜单示例
function Dropdown() {
  const [isOpen, setIsOpen] = useState(false);
  
  useEffect(() => {
    const handleClickOutside = () => {
      setIsOpen(false);
    };
    
    if (isOpen) {
      document.addEventListener('click', handleClickOutside);
    }
    
    return () => {
      document.removeEventListener('click', handleClickOutside);
    };
  }, [isOpen]);
  
  const handleToggle = (e) => {
    e.stopPropagation();
    setIsOpen(!isOpen);
  };
  
  const handleMenuClick = (e) => {
    e.stopPropagation();  // 点击菜单项不关闭
  };
  
  return (
    <div>
      <button onClick={handleToggle}>
        菜单
      </button>
      {isOpen && (
        <div className="menu" onClick={handleMenuClick}>
          <div>选项1</div>
          <div>选项2</div>
          <div>选项3</div>
        </div>
      )}
    </div>
  );
}

4.3 stopPropagation vs stopImmediatePropagation

jsx
function PropagationDifference() {
  const handleClick1 = (e) => {
    console.log('处理器1');
    e.stopPropagation();  // 阻止冒泡,但同元素的其他处理器仍会执行
  };
  
  const handleClick2 = () => {
    console.log('处理器2');  // 仍会执行
  };
  
  const handleClick3 = (e) => {
    console.log('处理器3');
    e.stopImmediatePropagation();  // 阻止冒泡,且阻止同元素的其他处理器
  };
  
  const handleClick4 = () => {
    console.log('处理器4');  // 不会执行
  };
  
  return (
    <div>
      <button
        onClick={handleClick1}
        onClickCapture={handleClick2}
      >
        stopPropagation(处理器2会执行)
      </button>
      
      <button
        onClick={handleClick3}
        onClickCapture={handleClick4}
      >
        stopImmediatePropagation(处理器4不执行)
      </button>
    </div>
  );
}

第五部分:事件处理的性能优化

5.1 避免内联函数

jsx
// 不好:每次渲染创建新函数
function Bad() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <ChildComponent onClick={() => setCount(count + 1)} />
    </div>
  );
}

// 好:提取为单独函数
function Good() {
  const [count, setCount] = useState(0);
  
  const handleClick = () => {
    setCount(count + 1);
  };
  
  return (
    <div>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

// 更好:使用useCallback
function Better() {
  const [count, setCount] = useState(0);
  
  const handleClick = useCallback(() => {
    setCount(c => c + 1);
  }, []);
  
  return (
    <div>
      <ChildComponent onClick={handleClick} />
    </div>
  );
}

5.2 事件委托

jsx
// 不好:每个项都绑定事件
function BadList({ items }) {
  const handleItemClick = (id) => {
    console.log('点击:', id);
  };
  
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => handleItemClick(item.id)}>
          {item.text}
        </li>
      ))}
    </ul>
  );
}

// 好:事件委托到父元素
function GoodList({ items }) {
  const handleListClick = (e) => {
    const id = e.target.dataset.id;
    if (id) {
      console.log('点击:', id);
    }
  };
  
  return (
    <ul onClick={handleListClick}>
      {items.map(item => (
        <li key={item.id} data-id={item.id}>
          {item.text}
        </li>
      ))}
    </ul>
  );
}

// React 19:自动事件委托优化
// React会自动将事件委托到root元素

5.3 节流和防抖

jsx
import { useState, useCallback } from 'react';

// 节流(throttle):固定时间间隔执行
function useThrottle(fn, delay) {
  const timeoutRef = useRef(null);
  const lastRunRef = useRef(0);
  
  return useCallback((...args) => {
    const now = Date.now();
    
    if (now - lastRunRef.current >= delay) {
      fn(...args);
      lastRunRef.current = now;
    }
  }, [fn, delay]);
}

function ThrottleExample() {
  const [scrollY, setScrollY] = useState(0);
  
  const handleScroll = useThrottle(() => {
    setScrollY(window.scrollY);
  }, 100);  // 每100ms最多执行一次
  
  useEffect(() => {
    window.addEventListener('scroll', handleScroll);
    return () => window.removeEventListener('scroll', handleScroll);
  }, [handleScroll]);
  
  return <div>滚动位置: {scrollY}</div>;
}

// 防抖(debounce):延迟执行,期间再次触发会重新计时
function useDebounce(fn, delay) {
  const timeoutRef = useRef(null);
  
  return useCallback((...args) => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    
    timeoutRef.current = setTimeout(() => {
      fn(...args);
    }, delay);
  }, [fn, delay]);
}

function DebounceExample() {
  const [searchTerm, setSearchTerm] = useState('');
  const [results, setResults] = useState([]);
  
  const performSearch = useDebounce((term) => {
    // 实际搜索逻辑
    fetch(`/api/search?q=${term}`)
      .then(r => r.json())
      .then(setResults);
  }, 300);  // 停止输入300ms后执行
  
  const handleChange = (e) => {
    const value = e.target.value;
    setSearchTerm(value);
    performSearch(value);
  };
  
  return (
    <div>
      <input value={searchTerm} onChange={handleChange} />
      <ul>
        {results.map(r => <li key={r.id}>{r.name}</li>)}
      </ul>
    </div>
  );
}

第六部分:React 19的事件处理新特性

6.1 自动事件委托优化

jsx
// React 19自动优化事件委托
function AutoDelegation() {
  const [items] = useState(Array(1000).fill(0).map((_, i) => ({
    id: i,
    text: `Item ${i}`
  })));
  
  const handleClick = (id) => {
    console.log('点击:', id);
  };
  
  // React 19会自动优化,不会为每个项创建单独的监听器
  return (
    <ul>
      {items.map(item => (
        <li key={item.id} onClick={() => handleClick(item.id)}>
          {item.text}
        </li>
      ))}
    </ul>
  );
}

6.2 Server Actions中的事件

jsx
// Server Action
'use server';

async function submitForm(formData) {
  const data = {
    name: formData.get('name'),
    email: formData.get('email')
  };
  
  await db.users.create(data);
  revalidatePath('/users');
  
  return { success: true };
}

// Client Component
'use client';

import { useActionState } from 'react';

function FormWithServerAction() {
  const [state, formAction, isPending] = useActionState(submitForm, null);
  
  return (
    <form action={formAction}>
      <input name="name" required />
      <input name="email" type="email" required />
      <button type="submit" disabled={isPending}>
        {isPending ? '提交中...' : '提交'}
      </button>
      {state?.success && <p>提交成功!</p>}
    </form>
  );
}

6.3 useTransition与事件

jsx
import { useState, useTransition } from 'react';

function TransitionEvent() {
  const [input, setInput] = useState('');
  const [list, setList] = useState([]);
  const [isPending, startTransition] = useTransition();
  
  const handleChange = (e) => {
    const value = e.target.value;
    
    // 高优先级:立即更新输入框
    setInput(value);
    
    // 低优先级:延迟更新列表
    startTransition(() => {
      const newList = generateLargeList(value);
      setList(newList);
    });
  };
  
  return (
    <div>
      <input value={input} onChange={handleChange} />
      {isPending && <div>更新中...</div>}
      <ul>
        {list.map((item, i) => <li key={i}>{item}</li>)}
      </ul>
    </div>
  );
}

第七部分:常见问题与解决

7.1 事件处理器中的异步操作

jsx
function AsyncEventHandler() {
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  
  const handleClick = async () => {
    setLoading(true);
    setError(null);
    
    try {
      const response = await fetch('/api/data');
      const data = await response.json();
      console.log(data);
    } catch (err) {
      setError(err.message);
    } finally {
      setLoading(false);
    }
  };
  
  return (
    <div>
      <button onClick={handleClick} disabled={loading}>
        {loading ? '加载中...' : '获取数据'}
      </button>
      {error && <p>错误: {error}</p>}
    </div>
  );
}

7.2 事件对象的持久化

jsx
function EventPersistence() {
  const handleClick = (e) => {
    // React事件对象会被重用
    console.log(e.target);  // 正常工作
    
    setTimeout(() => {
      console.log(e.target);  // 可能是null
    }, 100);
  };
  
  // 解决方案1:保存需要的值
  const handleClickSafe = (e) => {
    const target = e.target;
    
    setTimeout(() => {
      console.log(target);  // 正常工作
    }, 100);
  };
  
  // 解决方案2:持久化事件对象(不推荐)
  const handleClickPersist = (e) => {
    e.persist();  // React 17+已废弃
    
    setTimeout(() => {
      console.log(e.target);
    }, 100);
  };
  
  return <button onClick={handleClickSafe}>点击</button>;
}

7.3 表单提交问题

jsx
function FormSubmitIssues() {
  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 问题:直接访问e.target.elements
    const form = e.target;
    const username = form.elements.username.value;
    const password = form.elements.password.value;
    
    console.log({ username, password });
  };
  
  // 更好的方式:使用FormData
  const handleSubmitBetter = (e) => {
    e.preventDefault();
    
    const formData = new FormData(e.target);
    const data = Object.fromEntries(formData);
    
    console.log(data);
  };
  
  // 最佳:受控组件
  const [formData, setFormData] = useState({
    username: '',
    password: ''
  });
  
  const handleSubmitBest = (e) => {
    e.preventDefault();
    console.log(formData);
  };
  
  return (
    <form onSubmit={handleSubmitBest}>
      <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>
  );
}

第八部分:最佳实践总结

8.1 事件绑定清单

jsx
// 1. 函数组件:直接定义或useCallback
const handleClick = useCallback(() => {}, []);

// 2. 类组件:类字段箭头函数
handleClick = () => {};

// 3. 避免内联函数(性能敏感场景)
<Component onClick={handleClick} />  // 好
<Component onClick={() => {}} />     // 不好

// 4. 命名规范
const handleClick = () => {};        // 本地处理器
const onClick = () => {};            // props回调

// 5. 阻止默认行为
e.preventDefault();

// 6. 阻止冒泡
e.stopPropagation();

// 7. 异步处理要考虑清理
useEffect(() => {
  const handler = () => {};
  window.addEventListener('event', handler);
  return () => window.removeEventListener('event', handler);
}, []);

8.2 性能优化清单

jsx
// 1. 使用useCallback缓存事件处理器
// 2. 避免在render中绑定this
// 3. 大列表使用事件委托
// 4. 频繁事件使用节流/防抖
// 5. React.memo配合稳定的事件处理器

练习题

基础练习

  1. 创建一个按钮,点击时更新计数
  2. 实现输入框的实时值显示
  3. 创建一个表单,阻止默认提交

进阶练习

  1. 实现事件冒泡和阻止冒泡的示例
  2. 创建一个模态框,点击背景关闭
  3. 实现防抖搜索功能

高级练习

  1. 实现一个复杂的表单验证系统
  2. 创建一个支持拖拽的列表
  3. 使用React 19的新特性优化事件处理

通过本章学习,你已经全面掌握了React事件绑定的基础知识。事件处理是React交互的核心,理解这些概念对构建动态应用至关重要。继续学习,深入探索事件系统!