React 事件绑定的方式有哪些?区别?

一、是什么

在 React 中,事件绑定是组件与用户交互的基础。由于 React 组件有类组件和函数组件两种形式,事件绑定的方式和需要注意的问题也有所不同。

核心问题集中在两点:

  1. 类组件中的 this 指向:普通方法中 this 默认不指向组件实例
  2. 函数组件中的引用稳定性:每次渲染创建新函数可能导致不必要的子组件重渲染

二、类组件的事件绑定

2.1 构造函数中 bind 绑定

class Counter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { count: 0 };
    this.handleClick = this.handleClick.bind(this);
    this.handleReset = this.handleReset.bind(this);
  }

  handleClick() {
    this.setState((prev) => ({ count: prev.count + 1 }));
  }

  handleReset() {
    this.setState({ count: 0 });
  }

  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.handleClick}>+1</button>
        <button onClick={this.handleReset}>重置</button>
      </div>
    );
  }
}

优点

  • 每个方法只在构造函数中绑定一次,后续渲染不会创建新函数
  • 传递给子组件时引用稳定,不会引起不必要的重渲染

缺点

  • 代码冗长,每个事件方法都需要在 constructor 中手动 bind
  • 方法定义和绑定分散在不同位置,可读性差

2.2 Class Fields 箭头函数(推荐)

class Counter extends React.Component {
  state = { count: 0 };

  handleClick = () => {
    this.setState((prev) => ({ count: prev.count + 1 }));
  };

  handleReset = () => {
    this.setState({ count: 0 });
  };

  render() {
    return (
      <div>
        <p>{this.state.count}</p>
        <button onClick={this.handleClick}>+1</button>
        <button onClick={this.handleReset}>重置</button>
      </div>
    );
  }
}

原理:箭头函数没有自己的 this,它会捕获定义时的外层 this。Class fields 中的箭头函数在实例创建时被赋值到实例上,this 固定指向组件实例。

优点

  • 代码简洁,定义即绑定
  • 不需要 constructor
  • 引用稳定,适合作为 props 传递

缺点

  • 每个实例都会创建独立的函数副本(不在原型上共享)
  • 对于大量实例的场景(极少见)内存占用略高

2.3 render 中的箭头函数 / bind

class Counter extends React.Component {
  state = { count: 0 };

  handleClick(increment) {
    this.setState((prev) => ({ count: prev.count + increment }));
  }

  render() {
    return (
      <div>
        {/* 箭头函数:方便传参,但每次 render 创建新函数 */}
        <button onClick={() => this.handleClick(1)}>+1</button>
        {/* bind:效果相同,也是每次创建新函数 */}
        <button onClick={this.handleClick.bind(this, 5)}>+5</button>
      </div>
    );
  }
}

这两种方式的问题是:每次渲染都创建新函数引用,传给使用了 React.memo / PureComponent 的子组件时会导致不必要的重渲染。

2.5 类组件方式对比

方式代码简洁度引用稳定传参方便推荐度
constructor bind
class fields 箭头函数
render 中箭头函数
render 中 bind

三、函数组件的事件绑定

函数组件没有 this 的问题,但面临函数引用稳定性的挑战。

3.1 内联箭头函数

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>{count}</p>
      <button onClick={() => setCount((c) => c + 1)}>+1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

对于简单的事件处理逻辑,内联箭头函数是最简洁的写法。在大多数场景下性能完全没问题。

3.2 独立函数声明

function SearchForm() {
  const [query, setQuery] = useState("");
  const [results, setResults] = useState([]);

  function handleSubmit(e) {
    e.preventDefault();
    fetchResults(query).then(setResults);
  }

  function handleClear() {
    setQuery("");
    setResults([]);
  }

  return (
    <form onSubmit={handleSubmit}>
      <input value={query} onChange={(e) => setQuery(e.target.value)} />
      <button type="submit">搜索</button>
      <button type="button" onClick={handleClear}>清空</button>
    </form>
  );
}

适合逻辑稍复杂的事件处理。注意每次渲染都会重新创建这些函数,但这通常不是性能瓶颈。

3.3 useCallback 稳定引用

当事件处理函数需要传递给被 React.memo 包裹的子组件时,使用 useCallback 保持引用稳定:

const ExpensiveList = React.memo(function ExpensiveList({ items, onItemClick }) {
  console.log("ExpensiveList rendered");
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => onItemClick(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
});

function App() {
  const [selectedId, setSelectedId] = useState(null);
  const [filter, setFilter] = useState("");
  const items = useItems();

  // 不使用 useCallback: 每次 filter 变化都会导致 ExpensiveList 重新渲染
  // const handleItemClick = (id) => setSelectedId(id);

  // 使用 useCallback: 引用稳定,ExpensiveList 不会因 filter 变化而重新渲染
  const handleItemClick = useCallback((id) => {
    setSelectedId(id);
  }, []);

  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <ExpensiveList items={items} onItemClick={handleItemClick} />
      {selectedId && <Detail id={selectedId} />}
    </div>
  );
}

3.4 useCallback 的注意事项

function SearchResults({ query }) {
  const [page, setPage] = useState(1);

  // 依赖项变化时函数会重新创建
  const handleLoadMore = useCallback(() => {
    fetchResults(query, page + 1).then((data) => {
      setPage((p) => p + 1);
    });
  }, [query, page]);

  // 如果依赖经常变化,useCallback 就失去了意义
  // 此时可以用 useRef 保存最新值
  const pageRef = useRef(page);
  pageRef.current = page;

  const stableHandleLoadMore = useCallback(() => {
    fetchResults(query, pageRef.current + 1).then(() => {
      setPage((p) => p + 1);
    });
  }, [query]); // 只依赖 query

  return (
    <div>
      <ResultList items={results} />
      <button onClick={stableHandleLoadMore}>加载更多</button>
    </div>
  );
}

3.5 React 19 Compiler 的影响

React 19 引入的 React Compiler 可以自动进行记忆化优化:

// React 19 + Compiler: 不需要手动 useCallback
function App() {
  const [selectedId, setSelectedId] = useState(null);
  const [filter, setFilter] = useState("");

  // Compiler 自动分析依赖,在需要时进行记忆化
  const handleItemClick = (id) => {
    setSelectedId(id);
  };

  return (
    <div>
      <input value={filter} onChange={(e) => setFilter(e.target.value)} />
      <ExpensiveList onItemClick={handleItemClick} />
    </div>
  );
}

在 React Compiler 普及之后,useCallbackuseMemo 将逐渐成为不需要手动编写的优化。

四、传递参数的最佳实践

列表场景中需要为每个项传递不同参数,有两种常用方案:

// 方案一:内联箭头函数(简单场景,够用)
function ItemList({ items }) {
  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} onClick={() => console.log(item.id)}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

// 方案二:data 属性 + 单一处理函数(性能敏感场景)
function ItemList({ items }) {
  const handleClick = useCallback((e) => {
    console.log(e.currentTarget.dataset.id);
  }, []);

  return (
    <ul>
      {items.map((item) => (
        <li key={item.id} data-id={item.id} onClick={handleClick}>
          {item.name}
        </li>
      ))}
    </ul>
  );
}

五、性能指导原则

  1. 不要过早优化:内联箭头函数在绑定到原生元素(div、button 等)时几乎没有性能影响。只有传递给被 memo 的子组件时才需要考虑引用稳定性

  2. 优先使用 class fields 箭头函数(类组件)或独立函数声明(函数组件)

  3. useCallback 的使用场景

    • 作为 props 传递给 React.memo 包裹的子组件
    • 作为其他 Hook 的依赖项(如 useEffect 的依赖)
    • 自定义 Hook 返回的函数
  4. React 19 Compiler:如果项目使用了 React Compiler,大部分手动记忆化将不再需要

六、总结

场景推荐方式
类组件一般事件class fields 箭头函数
类组件需传参class fields + 提取子组件
函数组件简单事件内联箭头函数或独立声明
函数组件传给 memo 子组件useCallback
React 19 + Compiler自动优化,无需手动 useCallback

面试要点:能解释类组件中 this 丢失的原因(普通函数的 this 由调用方式决定),能说明各种绑定方式的性能影响,理解 useCallback 的适用场景和局限性,了解 React Compiler 对事件绑定优化的影响。