MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

React 事件监听器的添加与移除

2022-05-064.1k 阅读

React 事件监听器基础概念

在 React 开发中,事件监听器是实现交互功能的重要组成部分。React 对原生 DOM 事件进行了封装,提供了一套统一的、跨浏览器兼容的事件处理系统。与传统的直接在 DOM 元素上添加事件监听器不同,React 使用一种声明式的方式来处理事件。

例如,在原生 JavaScript 中,我们可能这样为一个按钮添加点击事件监听器:

<button id="myButton">点击我</button>
<script>
  const button = document.getElementById('myButton');
  button.addEventListener('click', function() {
    console.log('按钮被点击了');
  });
</script>

而在 React 中,代码如下:

import React, { Component } from 'react';

class ButtonComponent extends Component {
  handleClick = () => {
    console.log('按钮被点击了');
  }

  render() {
    return <button onClick={this.handleClick}>点击我</button>;
  }
}

export default ButtonComponent;

这里的 onClick 就是 React 封装的事件监听器,它接受一个函数作为值,当按钮被点击时,该函数就会被调用。

React 支持的事件类型非常丰富,包括但不限于鼠标事件(如 onClickonMouseOveronMouseOut 等)、键盘事件(如 onKeyDownonKeyUp 等)、表单事件(如 onChangeonSubmit 等)。

React 事件监听器的合成事件机制

React 并没有直接将事件监听器绑定到真实的 DOM 元素上。为了提高性能和更好地管理事件,React 使用了合成事件(SyntheticEvent)机制。

合成事件是 React 对原生浏览器事件的跨浏览器包装。当一个事件发生时,React 会首先捕获这个事件,然后将其封装成合成事件对象,传递给相应的事件处理函数。

合成事件具有和原生事件相似的接口,例如 event.target 可以获取触发事件的目标元素,event.preventDefault() 可以阻止默认行为等。

下面是一个使用合成事件阻止链接默认行为的例子:

import React, { Component } from 'react';

class LinkComponent extends Component {
  handleClick = (event) => {
    event.preventDefault();
    console.log('链接点击被阻止');
  }

  render() {
    return <a href="#" onClick={this.handleClick}>点击我</a>;
  }
}

export default LinkComponent;

合成事件在不同浏览器中保持一致的行为,并且通过事件委托机制,React 可以在文档的根节点上统一处理所有事件,从而减少内存开销。

在类组件中添加事件监听器

在类组件中添加事件监听器是比较常见的操作。前面已经展示了一个简单的按钮点击事件的处理,下面我们再详细分析一下其中的细节。

定义事件处理函数

在类组件中,事件处理函数通常定义为类的方法。例如:

import React, { Component } from 'react';

class CounterComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
    this.incrementCount = this.incrementCount.bind(this);
  }

  incrementCount() {
    this.setState((prevState) => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return <button onClick={this.incrementCount}>点击增加计数 {this.state.count}</button>;
  }
}

export default CounterComponent;

这里在 constructor 中使用 bind 方法将 incrementCount 方法绑定到组件实例 this 上。这是因为在 JavaScript 中,类方法默认不会绑定 this,如果不进行绑定,在事件处理函数被调用时,this 的指向可能会不正确,导致无法正确访问组件的 state 和其他方法。

另一种避免手动绑定 this 的方式是使用箭头函数定义事件处理函数,因为箭头函数没有自己的 this,它会继承外层作用域的 this

import React, { Component } from 'react';

class CounterComponent extends Component {
  state = {
    count: 0
  };

  incrementCount = () => {
    this.setState((prevState) => ({
      count: prevState.count + 1
    }));
  }

  render() {
    return <button onClick={this.incrementCount}>点击增加计数 {this.state.count}</button>;
  }
}

export default CounterComponent;

传递参数给事件处理函数

有时候我们需要在事件处理函数中传递额外的参数。例如,我们有一个列表,每个列表项都有一个唯一的 ID,当点击列表项时,我们希望知道是哪个 ID 的项被点击了。

import React, { Component } from 'react';

class ListComponent extends Component {
  handleItemClick = (id) => {
    console.log(`点击了 ID 为 ${id} 的列表项`);
  }

  render() {
    const items = [1, 2, 3, 4, 5];
    return (
      <ul>
        {items.map(item => (
          <li key={item} onClick={() => this.handleItemClick(item)}>{item}</li>
        ))}
      </ul>
    );
  }
}

export default ListComponent;

这里通过在 onClick 中使用箭头函数调用 handleItemClick 并传递 item 作为参数。

在函数组件中添加事件监听器

随着 React Hooks 的引入,函数组件的功能变得越来越强大,添加事件监听器也变得更加简洁。

使用 useState 和 useEffect 实现事件处理

例如,我们要实现一个简单的切换开关功能:

import React, { useState, useEffect } from'react';

const ToggleComponent = () => {
  const [isOn, setIsOn] = useState(false);

  const handleToggle = () => {
    setIsOn(!isOn);
  }

  useEffect(() => {
    console.log(`开关状态: ${isOn? '开' : '关'}`);
  }, [isOn]);

  return (
    <button onClick={handleToggle}>
      {isOn? '关闭' : '打开'}
    </button>
  );
}

export default ToggleComponent;

这里使用 useState 来管理开关的状态,handleToggle 函数作为点击事件的处理函数来切换状态。useEffect 用于在状态变化时执行副作用操作,这里是打印开关状态。

传递参数的处理

与类组件类似,在函数组件中传递参数给事件处理函数也可以使用箭头函数。例如:

import React, { useState } from'react';

const ParameterComponent = () => {
  const [message, setMessage] = useState('');

  const handleClick = (text) => {
    setMessage(text);
  }

  return (
    <div>
      <button onClick={() => handleClick('按钮 1 被点击')}>按钮 1</button>
      <button onClick={() => handleClick('按钮 2 被点击')}>按钮 2</button>
      <p>{message}</p>
    </div>
  );
}

export default ParameterComponent;

事件监听器的移除

在 React 中,对于大多数由 React 自身管理的事件监听器,React 会在组件卸载时自动移除它们,不需要开发者手动处理。例如前面提到的 onClick 等事件监听器,当组件从 DOM 中移除时,React 会负责清理相关的事件绑定。

然而,在某些情况下,我们可能需要手动添加一些原生的事件监听器,比如在浏览器窗口大小变化时执行某些操作。这时就需要手动移除这些事件监听器,以避免内存泄漏。

在类组件中手动移除事件监听器

以监听窗口滚动事件为例:

import React, { Component } from'react';

class ScrollComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      scrollY: 0
    };
  }

  componentDidMount() {
    window.addEventListener('scroll', this.handleScroll);
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', this.handleScroll);
  }

  handleScroll = () => {
    this.setState({
      scrollY: window.pageYOffset
    });
  }

  render() {
    return (
      <div>
        <p>当前滚动位置: {this.state.scrollY}</p>
      </div>
    );
  }
}

export default ScrollComponent;

componentDidMount 生命周期方法中,我们添加了窗口滚动事件监听器 window.addEventListener('scroll', this.handleScroll)。而在 componentWillUnmount 方法中,我们通过 window.removeEventListener('scroll', this.handleScroll) 移除了这个监听器。这样在组件卸载时,就不会因为事件监听器仍然绑定而导致内存泄漏。

在函数组件中手动移除事件监听器

使用 useEffect 可以在函数组件中实现类似的功能。例如,监听窗口大小变化:

import React, { useState, useEffect } from'react';

const WindowSizeComponent = () => {
  const [windowWidth, setWindowWidth] = useState(window.innerWidth);
  const [windowHeight, setWindowHeight] = useState(window.innerHeight);

  const handleResize = () => {
    setWindowWidth(window.innerWidth);
    setWindowHeight(window.innerHeight);
  }

  useEffect(() => {
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return (
    <div>
      <p>窗口宽度: {windowWidth}</p>
      <p>窗口高度: {windowHeight}</p>
    </div>
  );
}

export default WindowSizeComponent;

useEffect 中,我们添加了窗口大小变化的事件监听器 window.addEventListener('resize', handleResize)useEffect 返回的函数会在组件卸载时执行,在这里我们移除了事件监听器 window.removeEventListener('resize', handleResize)

复杂场景下的事件监听器添加与移除

动态添加和移除事件监听器

有时候我们可能需要根据组件的状态或其他条件动态地添加和移除事件监听器。例如,只有当某个按钮被点击后才开始监听窗口滚动事件。

import React, { Component } from'react';

class DynamicScrollComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isListening: false,
      scrollY: 0
    };
    this.handleButtonClick = this.handleButtonClick.bind(this);
    this.handleScroll = this.handleScroll.bind(this);
  }

  handleButtonClick() {
    this.setState((prevState) => ({
      isListening:!prevState.isListening
    }), () => {
      if (this.state.isListening) {
        window.addEventListener('scroll', this.handleScroll);
      } else {
        window.removeEventListener('scroll', this.handleScroll);
      }
    });
  }

  handleScroll() {
    this.setState({
      scrollY: window.pageYOffset
    });
  }

  render() {
    return (
      <div>
        <button onClick={this.handleButtonClick}>
          {this.state.isListening? '停止监听' : '开始监听'}
        </button>
        {this.state.isListening && <p>当前滚动位置: {this.state.scrollY}</p>}
      </div>
    );
  }
}

export default DynamicScrollComponent;

在这个例子中,点击按钮会切换 isListening 的状态,根据这个状态来动态地添加或移除窗口滚动事件监听器。

多个事件监听器的管理

在一些复杂的组件中,可能需要同时管理多个不同类型的事件监听器。例如,一个绘图组件可能需要同时监听鼠标移动、鼠标按下和鼠标释放事件。

import React, { Component } from'react';

class DrawingComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      isDrawing: false,
      startX: 0,
      startY: 0,
      endX: 0,
      endY: 0
    };
    this.handleMouseDown = this.handleMouseDown.bind(this);
    this.handleMouseMove = this.handleMouseMove.bind(this);
    this.handleMouseUp = this.handleMouseUp.bind(this);
  }

  componentDidMount() {
    document.addEventListener('mousedown', this.handleMouseDown);
    document.addEventListener('mousemove', this.handleMouseMove);
    document.addEventListener('mouseup', this.handleMouseUp);
  }

  componentWillUnmount() {
    document.removeEventListener('mousedown', this.handleMouseDown);
    document.removeEventListener('mousemove', this.handleMouseMove);
    document.removeEventListener('mouseup', this.handleMouseUp);
  }

  handleMouseDown(event) {
    this.setState({
      isDrawing: true,
      startX: event.clientX,
      startY: event.clientY
    });
  }

  handleMouseMove(event) {
    if (this.state.isDrawing) {
      this.setState({
        endX: event.clientX,
        endY: event.clientY
      });
    }
  }

  handleMouseUp() {
    this.setState({
      isDrawing: false
    });
  }

  render() {
    return (
      <div>
        {/* 这里可以根据 state 中的坐标信息进行绘图 */}
      </div>
    );
  }
}

export default DrawingComponent;

componentDidMount 中添加了三个不同的事件监听器,在 componentWillUnmount 中相应地移除这些监听器。

事件监听器与性能优化

事件节流与防抖

在处理一些高频触发的事件时,如滚动、窗口大小变化等,如果不进行处理,可能会导致性能问题。事件节流(Throttle)和防抖(Debounce)是两种常用的优化技术。

事件节流: 节流会限制事件的触发频率,确保在一定时间间隔内事件处理函数只被调用一次。例如,我们可以使用 lodash 库中的 throttle 函数来实现滚动事件的节流。

import React, { Component } from'react';
import throttle from 'lodash/throttle';

class ThrottleScrollComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      scrollY: 0
    };
    this.handleScroll = this.handleScroll.bind(this);
  }

  componentDidMount() {
    window.addEventListener('scroll', throttle(this.handleScroll, 200));
  }

  componentWillUnmount() {
    window.removeEventListener('scroll', throttle(this.handleScroll, 200));
  }

  handleScroll() {
    this.setState({
      scrollY: window.pageYOffset
    });
  }

  render() {
    return (
      <div>
        <p>当前滚动位置: {this.state.scrollY}</p>
      </div>
    );
  }
}

export default ThrottleScrollComponent;

这里 throttle(this.handleScroll, 200) 表示每 200 毫秒最多调用一次 handleScroll 函数,即使在这 200 毫秒内滚动事件触发了多次。

事件防抖: 防抖会在事件触发后等待一定时间,如果在这段时间内事件再次触发,则重新计时,只有在指定时间内没有再次触发事件时,才会执行事件处理函数。同样使用 lodash 库中的 debounce 函数来实现输入框输入的防抖。

import React, { Component } from'react';
import debounce from 'lodash/debounce';

class DebounceInputComponent extends Component {
  constructor(props) {
    super(props);
    this.state = {
      inputValue: ''
    };
    this.handleInputChange = this.handleInputChange.bind(this);
    this.debouncedSearch = debounce(this.search, 500);
  }

  componentWillUnmount() {
    this.debouncedSearch.cancel();
  }

  handleInputChange(event) {
    this.setState({
      inputValue: event.target.value
    });
    this.debouncedSearch(event.target.value);
  }

  search(value) {
    console.log(`搜索: ${value}`);
  }

  render() {
    return (
      <div>
        <input
          type="text"
          value={this.state.inputValue}
          onChange={this.handleInputChange}
          placeholder="输入搜索内容"
        />
      </div>
    );
  }
}

export default DebounceInputComponent;

这里 debounce(this.search, 500) 表示在输入停止 500 毫秒后才会调用 search 函数,如果在 500 毫秒内又有新的输入,则重新计时。

减少不必要的事件绑定

在 React 中,尽量避免在 render 方法中动态创建事件处理函数,因为每次 render 时都会创建新的函数实例,这可能会导致不必要的重新渲染。例如:

import React, { Component } from'react';

class BadPracticeComponent extends Component {
  state = {
    count: 0
  };

  render() {
    return <button onClick={() => this.setState({ count: this.state.count + 1 })}>点击增加计数 {this.state.count}</button>;
  }
}

export default BadPracticeComponent;

这种写法每次 render 时都会创建一个新的箭头函数作为 onClick 的处理函数,这会导致依赖这个函数引用的组件不必要地重新渲染。

更好的做法是在类的构造函数或使用箭头函数定义事件处理函数,如前面提到的:

import React, { Component } from'react';

class GoodPracticeComponent extends Component {
  state = {
    count: 0
  };

  incrementCount = () => {
    this.setState({ count: this.state.count + 1 });
  }

  render() {
    return <button onClick={this.incrementCount}>点击增加计数 {this.state.count}</button>;
  }
}

export default GoodPracticeComponent;

这样事件处理函数的引用在组件的生命周期内保持不变,避免了不必要的重新渲染。

总结

React 事件监听器的添加与移除是前端开发中实现交互功能的重要环节。通过深入理解 React 的合成事件机制、在类组件和函数组件中正确添加和移除事件监听器,以及掌握复杂场景下的处理和性能优化技巧,开发者能够编写出高效、稳定且交互性良好的 React 应用程序。在实际开发中,需要根据具体的业务需求和场景,合理地运用这些知识,确保应用的性能和用户体验。同时,随着 React 技术的不断发展,事件处理的方式和最佳实践可能也会有所变化,开发者需要持续关注和学习。