3. React事件处理:合成事件、事件绑定、事件对象、自定义事件

事件处理,说白了就是用户跟页面交互的那座桥。你点一下按钮,敲一下键盘,页面得知道该干嘛。React 在这方面做了不少封装,跟原生 DOM 事件有些区别。我刚开始用 React 时,也踩过几个坑,今天咱们就好好捋一捋。

3.1 合成事件:React 的包装层

React 并没有直接把原生事件绑到 DOM 上。它搞了一套自己的事件系统,叫「合成事件」。你想想看,如果每个按钮都绑一个原生事件,那性能得多差?React 的做法是,在根节点上统一监听,然后通过事件委托分发。

核心要点:合成事件是原生事件的跨浏览器包装器,拥有与原生事件相同的接口,但抹平了浏览器差异。

举个例子,你在 JSX 里写 onClick,实际上触发的是 SyntheticEvent。它长这样:

function handleClick(event) {
  // event 是 SyntheticEvent,不是原生事件
  console.log(event.type);       // 'click'
  console.log(event.target);     // 触发事件的 DOM 元素
  console.log(event.nativeEvent); // 原生事件对象
}

我记得有一次排查 bug,发现 event.preventDefault() 在 IE 上失效了。后来才意识到,原生事件里得用 returnValue,但合成事件统一封装了,直接用 preventDefault() 就行。嗯,这就是合成事件的好处。

小技巧:合成事件是池化的。事件回调执行完后,属性会被清空。如果你需要异步访问事件对象,记得调用 event.persist()

3.2 事件绑定:JSX 里的写法

React 的事件绑定,说白了就是在 JSX 里写 on+事件名。驼峰命名,别写错了。比如 onClickonChangeonSubmit

我见过不少新手写成 onclick,结果事件死活不触发。这里要注意:

  • 传递函数引用onClick={handleClick},不是 onClick={handleClick()}
  • 绑定 this:类组件里记得 bind,或者用箭头函数
  • 传参:用箭头函数包裹,或者 bind 方法

看几个例子:

// 正确:传递函数引用
<button onClick={handleClick}>点击</button>

// 错误:这样会立即执行
<button onClick={handleClick()}>点击</button>

// 传参方式一:箭头函数
<button onClick={() => handleClick('参数')}>点击</button>

// 传参方式二:bind
<button onClick={handleClick.bind(null, '参数')}>点击</button>

我个人习惯用箭头函数,代码更直观。但要注意,箭头函数每次渲染都会创建新函数,如果子组件是 PureComponent 或用了 React.memo,可能会影响性能。这时候用 useCallback 包一下就好。

避坑指南:我曾经在循环里直接写 onClick={() => doSomething(item)},结果列表一长,性能明显下降。后来改用 data-* 属性 + 事件委托,才解决问题。

3.3 事件对象:你能拿到什么

事件处理函数接收一个参数——合成事件对象。它包含了所有你需要的信息。常用的属性有:

属性/方法 说明 示例
type 事件类型 'click'
target 触发事件的元素 <button>
currentTarget 绑定事件的元素 跟 target 可能不同
nativeEvent 原生事件对象 用于访问原生属性
preventDefault() 阻止默认行为 表单提交、链接跳转
stopPropagation() 阻止事件冒泡 避免父元素也触发

举个例子,表单提交时阻止页面刷新:

function handleSubmit(event) {
  event.preventDefault(); // 阻止默认提交行为
  // 然后自己处理数据
  console.log('表单数据:', formData);
}

还有事件冒泡的问题。你想想看,点击子元素,父元素的 onClick 也会触发。这时候 stopPropagation 就派上用场了:

function handleChildClick(event) {
  event.stopPropagation(); // 阻止冒泡
  console.log('只触发子元素');
}

function handleParentClick() {
  console.log('父元素不会触发');
}

个人经验:我在做弹窗组件时,经常用 stopPropagation 防止点击弹窗内部时关闭弹窗。但别滥用,过度阻止冒泡会让事件流变得难以调试。

3.4 自定义事件:组件间的通信

React 里没有原生自定义事件的概念。但我们可以通过 props 传递回调函数,实现类似的效果。说白了,就是父组件传一个函数给子组件,子组件在合适的时机调用它。

看个例子:

// 父组件
function Parent() {
  const handleCustomEvent = (data) => {
    console.log('收到子组件的数据:', data);
  };

  return <Child onCustomEvent={handleCustomEvent} />;
}

// 子组件
function Child({ onCustomEvent }) {
  const triggerEvent = () => {
    // 触发自定义事件
    onCustomEvent({ message: '你好,父组件!' });
  };

  return <button onClick={triggerEvent}>触发事件</button>;
}

这种方式其实就是「回调函数模式」。我习惯把回调命名为 onXxx,跟原生事件风格保持一致。比如 onSaveonDeleteonChange

如果组件层级很深,一层层传 props 会很痛苦。这时候可以用 Context 或者状态管理库。但别一上来就用全局状态,简单场景下回调就够了。

核心原则:自定义事件本质是「子组件通知父组件」。数据流向是单向的,从子到父通过回调,从父到子通过 props。

3.5 常见坑点与最佳实践

最后,分享几个我踩过的坑:

  • 异步访问事件对象:合成事件是池化的,回调执行完属性就没了。如果你在 setTimeout 里访问 event,会得到 null。解决办法:event.persist() 或提前保存需要的值。
  • 事件绑定性能:循环里别直接写箭头函数,用 useCallbackdata-* 属性。
  • 阻止默认行为:记得用 preventDefault,而不是 return false。React 里 return false 无效。
  • 事件命名:自定义事件用 onXxx 格式,保持一致性。

嗯,React 的事件处理其实不复杂。核心就是记住:合成事件是包装过的,绑定用驼峰命名,传参用箭头函数或 bind,自定义事件靠回调。把这些搞明白,日常开发基本够用了。

最后一个小建议:多看看 React 官方文档里关于合成事件的章节。虽然平时用不到所有事件,但了解全貌能帮你更快定位问题。