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

React 中的事件监听与 useEventListener

2021-07-166.0k 阅读

React 事件监听基础

在 React 应用开发中,事件监听是实现交互功能的重要手段。React 采用了一套合成事件(SyntheticEvent)机制,它并不是原生 DOM 事件的直接使用,而是对原生事件的一层封装。这样做有诸多好处,例如浏览器兼容性、性能优化以及统一的事件处理接口。

常见事件类型

React 支持多种类型的事件,涵盖了从用户交互到浏览器行为等多个方面。

  1. 鼠标事件:像 onClickonMouseEnteronMouseLeave 等。onClick 事件是最常用的,用于处理用户点击操作。例如,我们有一个按钮,当用户点击它时显示一条消息:
import React from 'react';

function ButtonComponent() {
  const handleClick = () => {
    console.log('按钮被点击了');
  };
  return <button onClick={handleClick}>点击我</button>;
}

export default ButtonComponent;
  1. 键盘事件onKeyDownonKeyUp 等。在输入框场景中,当用户按下键盘按键时可以触发相应操作。比如,实时显示用户输入的字符:
import React, { useState } from'react';

function InputComponent() {
  const [inputValue, setInputValue] = useState('');
  const handleKeyDown = (event) => {
    setInputValue(event.key);
  };
  return (
    <input
      type="text"
      onKeyDown={handleKeyDown}
      placeholder="按下按键显示字符"
    />
  );
}

export default InputComponent;
  1. 表单事件onChangeonSubmit 等。onChange 常用于表单元素,如 inputselect 等,当元素的值发生变化时触发。onSubmit 用于表单提交,以下是一个简单的登录表单示例:
import React, { useState } from'react';

function LoginForm() {
  const [username, setUsername] = useState('');
  const [password, setPassword] = useState('');
  const handleSubmit = (event) => {
    event.preventDefault();
    console.log(`用户名: ${username}, 密码: ${password}`);
  };
  return (
    <form onSubmit={handleSubmit}>
      <label>
        用户名:
        <input
          type="text"
          value={username}
          onChange={(e) => setUsername(e.target.value)}
        />
      </label>
      <label>
        密码:
        <input
          type="password"
          value={password}
          onChange={(e) => setPassword(e.target.value)}
        />
      </label>
      <input type="submit" value="登录" />
    </form>
  );
}

export default LoginForm;

事件处理函数绑定

在 React 中,将事件处理函数绑定到组件实例上是常见操作。通常有几种方式:

  1. 在构造函数中绑定:在类组件中,可以在 constructor 里使用 bind 方法绑定 this
import React, { Component } from'react';

class ClickComponent extends Component {
  constructor(props) {
    super(props);
    this.handleClick = this.handleClick.bind(this);
  }
  handleClick() {
    console.log('按钮被点击,来自类组件');
  }
  render() {
    return <button onClick={this.handleClick}>类组件按钮</button>;
  }
}

export default ClickComponent;
  1. 使用箭头函数:在 JSX 中直接使用箭头函数,它会自动捕获正确的 this 值。
import React, { Component } from'react';

class ArrowClickComponent extends Component {
  handleClick() {
    console.log('箭头函数绑定点击,来自类组件');
  }
  render() {
    return <button onClick={() => this.handleClick()}>箭头函数类组件按钮</button>;
  }
}

export default ArrowClickComponent;

在函数式组件中,由于不存在 this 的绑定问题,事件处理函数直接定义即可。

React 函数式组件中的事件监听

随着 React Hooks 的出现,函数式组件变得更加灵活和强大,事件监听的处理也有了新的方式。

useState 与事件监听结合

useState 是 React Hooks 中用于在函数式组件中添加状态的钩子。我们可以结合 useState 来处理事件并更新组件状态。例如,一个简单的计数器组件,每次点击按钮增加计数:

import React, { useState } from'react';

function CounterComponent() {
  const [count, setCount] = useState(0);
  const handleClick = () => {
    setCount(count + 1);
  };
  return (
    <div>
      <p>计数: {count}</p>
      <button onClick={handleClick}>增加计数</button>
    </div>
  );
}

export default CounterComponent;

这里,useState 初始化了 count 状态为 0,当按钮点击事件触发时,handleClick 函数通过 setCount 更新 count 的值,从而触发组件重新渲染。

useEffect 与事件监听

useEffect 钩子用于在函数式组件中执行副作用操作,它也可以用于事件监听。useEffect 接收两个参数,第一个是副作用函数,第二个是依赖数组。

  1. 简单的事件监听:假设我们要监听窗口大小变化,并在控制台打印窗口宽度。
import React, { useEffect } from'react';

function WindowSizeComponent() {
  useEffect(() => {
    const handleResize = () => {
      console.log(`窗口宽度: ${window.innerWidth}`);
    };
    window.addEventListener('resize', handleResize);
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  return <div>窗口大小监听组件</div>;
}

export default WindowSizeComponent;

在上述代码中,useEffect 的副作用函数里添加了 resize 事件监听器,依赖数组为空意味着这个副作用只在组件挂载时执行一次。返回的函数用于在组件卸载时移除事件监听器,防止内存泄漏。 2. 带依赖的事件监听:如果事件监听依赖于组件的某个状态,我们可以将该状态添加到依赖数组中。比如,根据一个开关状态决定是否监听滚动事件。

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

function ScrollComponent() {
  const [isListening, setIsListening] = useState(false);
  useEffect(() => {
    const handleScroll = () => {
      console.log('窗口滚动了');
    };
    if (isListening) {
      window.addEventListener('scroll', handleScroll);
    }
    return () => {
      window.removeEventListener('scroll', handleScroll);
    };
  }, [isListening]);
  return (
    <div>
      <input
        type="checkbox"
        onChange={() => setIsListening(!isListening)}
        checked={isListening}
      />
      <label>开启/关闭滚动监听</label>
    </div>
  );
}

export default ScrollComponent;

这里,依赖数组包含 isListening,当 isListening 状态变化时,useEffect 的副作用函数会重新执行,添加或移除滚动事件监听器。

useEventListener 自定义 Hook

虽然可以使用 useEffect 来实现事件监听,但为了提高代码的复用性和可维护性,我们可以创建一个自定义 Hook useEventListener

创建 useEventListener Hook

import { useEffect } from'react';

const useEventListener = (eventName, handler, element = window) => {
  useEffect(() => {
    const targetElement = element;
    const eventHandler = (event) => handler(event);
    targetElement.addEventListener(eventName, eventHandler);
    return () => {
      targetElement.removeEventListener(eventName, eventHandler);
    };
  }, [eventName, handler, element]);
};

export default useEventListener;

这个 useEventListener Hook 接收三个参数:eventName 表示要监听的事件名称,handler 是事件处理函数,element 是要绑定事件的目标元素,默认为 window。在 useEffect 中,它添加事件监听器,并在组件卸载时移除监听器。

使用 useEventListener Hook

  1. 监听窗口滚动事件
import React from'react';
import useEventListener from './useEventListener';

function ScrollWithHookComponent() {
  const handleScroll = () => {
    console.log('使用自定义 Hook 监听窗口滚动');
  };
  useEventListener('scroll', handleScroll);
  return <div>使用自定义 Hook 的滚动监听组件</div>;
}

export default ScrollWithHookComponent;
  1. 监听 DOM 元素的点击事件
import React, { useRef } from'react';
import useEventListener from './useEventListener';

function ClickOnElementComponent() {
  const elementRef = useRef(null);
  const handleClick = () => {
    console.log('自定义 DOM 元素被点击');
  };
  useEventListener('click', handleClick, elementRef.current);
  return (
    <div ref={elementRef}>
      点击我,使用自定义 Hook 监听
    </div>
  );
}

export default ClickOnElementComponent;

通过 useRef 获取 DOM 元素引用,并将其作为 useEventListener 的第三个参数,实现对特定 DOM 元素的事件监听。

事件监听中的性能优化

在 React 应用中,不合理的事件监听可能会导致性能问题,特别是在频繁触发事件的场景下。

防抖(Debounce)

防抖是指在事件触发一定时间后才执行回调函数,如果在这段时间内事件再次触发,则重新计时。在 React 中,可以通过自定义防抖函数结合事件监听来实现。

import React, { useState } from'react';

const debounce = (func, delay) => {
  let timer;
  return function() {
    const context = this;
    const args = arguments;
    clearTimeout(timer);
    timer = setTimeout(() => {
      func.apply(context, args);
    }, delay);
  };
};

function DebounceComponent() {
  const [inputValue, setInputValue] = useState('');
  const debouncedHandleChange = debounce((value) => {
    console.log('防抖后处理输入: ', value);
  }, 500);
  const handleChange = (event) => {
    const value = event.target.value;
    setInputValue(value);
    debouncedHandleChange(value);
  };
  return (
    <input
      type="text"
      value={inputValue}
      onChange={handleChange}
      placeholder="防抖输入框"
    />
  );
}

export default DebounceComponent;

在这个例子中,debounce 函数返回一个新的函数,当输入框 onChange 事件触发时,先设置输入值,然后调用防抖后的函数,这样只有在用户停止输入 500 毫秒后才会执行回调函数。

节流(Throttle)

节流是指在一定时间内,只允许事件触发一次回调函数。同样可以通过自定义节流函数来应用于事件监听。

import React, { useState } from'react';

const throttle = (func, limit) => {
  let inThrottle;
  return function() {
    const context = this;
    const args = arguments;
    if (!inThrottle) {
      func.apply(context, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
};

function ThrottleComponent() {
  const [inputValue, setInputValue] = useState('');
  const throttledHandleChange = throttle((value) => {
    console.log('节流后处理输入: ', value);
  }, 500);
  const handleChange = (event) => {
    const value = event.target.value;
    setInputValue(value);
    throttledHandleChange(value);
  };
  return (
    <input
      type="text"
      value={inputValue}
      onChange={handleChange}
      placeholder="节流输入框"
    />
  );
}

export default ThrottleComponent;

这里,throttle 函数返回的新函数在事件触发时,只有在 inThrottlefalse 时才执行回调,并设置 inThrottletrue,在 limit 毫秒后重置为 false,确保在这段时间内只执行一次回调。

事件监听与 React 组件通信

事件监听不仅用于处理用户交互,还可以在 React 组件之间进行通信。

父子组件通信

在父子组件通信中,父组件可以通过传递事件处理函数给子组件,子组件触发事件时调用父组件传递的函数来实现通信。例如,父组件有一个状态,子组件通过点击按钮改变父组件状态。

import React, { useState } from'react';

function ChildComponent({ handleClick }) {
  return <button onClick={handleClick}>子组件按钮</button>;
}

function ParentComponent() {
  const [message, setMessage] = useState('初始消息');
  const handleChildClick = () => {
    setMessage('子组件点击后更新的消息');
  };
  return (
    <div>
      <p>{message}</p>
      <ChildComponent handleClick={handleChildClick} />
    </div>
  );
}

export default ParentComponent;

父组件 ParentComponent 定义了 handleChildClick 函数并传递给子组件 ChildComponent,子组件按钮点击时调用该函数,从而更新父组件的 message 状态。

兄弟组件通信

兄弟组件之间通信可以通过共同的父组件作为桥梁。父组件将状态和事件处理函数传递给两个兄弟组件,一个兄弟组件触发事件改变父组件状态,另一个兄弟组件通过接收父组件传递的状态更新显示。例如,一个兄弟组件控制另一个兄弟组件的显示隐藏。

import React, { useState } from'react';

function SiblingOneComponent({ handleToggle }) {
  return <button onClick={handleToggle}>切换兄弟组件显示</button>;
}

function SiblingTwoComponent({ isVisible }) {
  return isVisible? <div>我是第二个兄弟组件</div> : null;
}

function ParentForSiblingsComponent() {
  const [isVisible, setIsVisible] = useState(false);
  const handleToggle = () => {
    setIsVisible(!isVisible);
  };
  return (
    <div>
      <SiblingOneComponent handleToggle={handleToggle} />
      <SiblingTwoComponent isVisible={isVisible} />
    </div>
  );
}

export default ParentForSiblingsComponent;

这里,ParentForSiblingsComponent 管理 isVisible 状态,并将 handleToggle 函数传递给 SiblingOneComponent,将 isVisible 状态传递给 SiblingTwoComponent,实现兄弟组件间的通信。

事件监听在 React 项目中的实际应用场景

  1. 实时数据更新:在实时聊天应用中,通过监听 WebSocket 事件,实时更新聊天消息列表。
import React, { useState, useEffect } from'react';

function ChatComponent() {
  const [messages, setMessages] = useState([]);
  useEffect(() => {
    const socket = new WebSocket('ws://localhost:8080');
    socket.addEventListener('message', (event) => {
      const newMessage = JSON.parse(event.data);
      setMessages([...messages, newMessage]);
    });
    return () => {
      socket.close();
    };
  }, []);
  return (
    <div>
      {messages.map((message, index) => (
        <p key={index}>{message}</p>
      ))}
    </div>
  );
}

export default ChatComponent;
  1. 动态布局调整:在响应式网页设计中,监听窗口大小变化事件,动态调整页面布局。
import React, { useState, useEffect } from'react';

function ResponsiveLayoutComponent() {
  const [layout, setLayout] = useState('default');
  useEffect(() => {
    const handleResize = () => {
      if (window.innerWidth < 600) {
        setLayout('mobile');
      } else {
        setLayout('desktop');
      }
    };
    window.addEventListener('resize', handleResize);
    handleResize();
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);
  return (
    <div>
      {layout ==='mobile'? (
        <p>移动布局</p>
      ) : (
        <p>桌面布局</p>
      )}
    </div>
  );
}

export default ResponsiveLayoutComponent;
  1. 用户行为跟踪:监听用户的点击、滚动等行为,记录用户操作日志,用于分析用户行为习惯。
import React, { useEffect } from'react';

function UserBehaviorComponent() {
  useEffect(() => {
    const handleClick = () => {
      console.log('用户点击了页面');
      // 这里可以添加发送日志到服务器的逻辑
    };
    const handleScroll = () => {
      console.log('用户滚动了页面');
      // 这里可以添加发送日志到服务器的逻辑
    };
    window.addEventListener('click', handleClick);
    window.addEventListener('scroll', handleScroll);
    return () => {
      window.removeEventListener('click', handleClick);
      window.removeEventListener('scroll', handleScroll);
    };
  }, []);
  return <div>用户行为跟踪组件</div>;
}

export default UserBehaviorComponent;

事件监听中的常见问题与解决方法

  1. 事件绑定多次:在使用 useEffect 监听事件时,如果依赖数组设置不当,可能会导致事件绑定多次。例如,依赖数组为空,但副作用函数内引用了组件状态,每次状态变化不会重新绑定事件,而如果依赖数组包含所有状态,可能会导致不必要的重复绑定。解决方法是仔细分析事件监听依赖的状态,只将必要的状态放入依赖数组。
  2. this 指向问题:在类组件中,如果没有正确绑定 this,事件处理函数中的 this 可能会指向错误的对象。可以使用构造函数中 bind 或者箭头函数来确保 this 指向正确。
  3. 事件穿透:在多层嵌套的元素中,可能会出现事件穿透问题,即子元素的事件触发后,父元素的相同事件也会触发。可以通过在子元素事件处理函数中调用 event.stopPropagation() 来阻止事件冒泡,避免事件穿透。

通过深入理解 React 中的事件监听机制以及 useEventListener 自定义 Hook 的使用,开发者能够更高效地实现交互功能,优化应用性能,并解决实际开发中遇到的各种问题。无论是简单的按钮点击,还是复杂的实时数据交互,事件监听都是 React 应用开发中不可或缺的一部分。