讲解

React 里绑事件是「传函数」而不是「写字符串」:<button onClick={handleClick}>。注意传的是函数引用,onClick={handleClick()} 是立刻调用并把返回值当回调——每次渲染都执行一遍,新手最高频的 bug 没有之一。需要传参就包一层箭头函数:onClick={() => handleDelete(id)}

事件回调拿到的 e 是 React 的合成事件(SyntheticEvent):它抹平了浏览器差异,接口和原生事件一致(e.target、e.preventDefault()、e.stopPropagation())。React 18 起事件委托到根容器统一监听,性能开销极小,你只管写业务逻辑。

表单类事件里 onChange 最常用:React 的 onChange 等价于原生 input 事件,每次输入都触发,配合状态就是受控组件(表单专章展开)。常见约定:处理函数以 handle 开头(handleSubmit、handleChange),作为 props 传下去时以 on 开头(onSubmit、onChange)——读代码时一眼分清「定义者」和「使用者」。

事件传播在 React 里照常工作:子元素点击会冒泡到父元素的 onClick。需要阻断时 e.stopPropagation(),比如弹窗「点遮罩关闭、点内容不关闭」。还有一个细节:事件回调里读状态,读到的是「触发那次渲染时」的快照——异步回调里状态可能已经变了,这类场景用 ref 或函数式更新解决(后面章节展开)。

示例

验证「传引用 vs 传调用结果」的区别,以及冒泡/阻止传播(在本教程构建时被真实执行):

import assert from 'node:assert/strict';

const log = [];

// onClick={handleClick} —— 传引用:只在点击时执行
function handleClick() {
  log.push('点击了');
}
const onClickCorrect = handleClick; // 渲染时不执行
assert.deepStrictEqual(log, []);

onClickCorrect(); // 模拟点击
assert.deepStrictEqual(log, ['点击了']);

// onClick={handleClick()} —— 传调用结果:渲染时就执行了
const onClickBuggy = handleClick(); // 立即执行!
assert.deepStrictEqual(log, ['点击了', '点击了']);
assert.strictEqual(onClickBuggy, undefined); // onClick 拿到的是 undefined

// 冒泡:子元素事件触发父元素处理器
function createEvent(target) {
  return {
    target,
    stopped: false,
    stopPropagation() {
      this.stopped = true;
    },
  };
}
function dispatch(e, handlers) {
  for (const h of handlers) {
    if (e.stopped) break; // 被 stopPropagation 切断
    h(e);
  }
}

const fired = [];
const e1 = createEvent('content');
dispatch(e1, [() => fired.push('内容'), () => fired.push('遮罩')]);
assert.deepStrictEqual(fired, ['内容', '遮罩']); // 冒泡到遮罩

const e2 = createEvent('content');
const fired2 = [];
dispatch(e2, [(e) => { fired2.push('内容'); e.stopPropagation(); }, () => fired2.push('遮罩')]);
assert.deepStrictEqual(fired2, ['内容']); // 遮罩没收到

console.log('传引用:点击才执行;传调用:渲染就执行且回调是 undefined');
console.log('冒泡默认触发父级;stopPropagation 后只剩:', fired2.join('、'));

真实组件:

function TodoItem({ todo, onDelete }) {
  return (
    <li>
      {todo.text}
      {/* 传参:包一层箭头函数 */}
      <button onClick={() => onDelete(todo.id)}>删除</button>
    </li>
  );
}

function Modal({ onClose, children }) {
  return (
    <div className="mask" onClick={onClose}>
      {/* 点内容不关闭:阻止冒泡到遮罩 */}
      <div className="body" onClick={(e) => e.stopPropagation()}>
        {children}
      </div>
    </div>
  );
}

常见坑

  • onClick={fn()} 立刻执行:要传函数本身或箭头函数包装,这是新手第一大坑。
  • 在渲染期间发请求/改状态:事件回调才是副作用的家,渲染函数必须纯净。
  • 异步回调里读旧状态:setTimeout 里的 state 是闭包快照,需要最新值用 ref 或函数式 setState。
  • event.persist() 老代码:React 17 起合成事件不再池化,persist 已无用,看到老代码可以删。
  • native 事件混用忘记清理:useEffect 里 addEventListener 必须配 removeEventListener(effect 章节细讲)。

小结

事件传函数引用,传参包箭头函数;合成事件接口同原生;冒泡可 stopPropagation;handle/on 命名约定分清定义与使用。下一章:让界面「记住事情」——useState。