You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React自定义Hook每次调用是否会重复添加事件监听器?如何优化?

问题解答

多个组件用这个Hook会加多个监听器吗?

没错,每个使用useMediaQuery的组件都会独立给window添加一个resize事件监听器。因为你Hook里的useEffect是空依赖,每次组件挂载时都会执行window.addEventListener('resize', listener),而且每个组件里的listener都是独立的函数引用,所以最终会注册一堆监听器。组件多了之后,每次窗口resize时所有监听器都会跑一遍,重复做matchMedia判断和状态更新,确实会浪费性能。

怎么避免重复监听?

核心思路就是全局只注册一次resize监听器,让所有用这个Hook的组件共享这个监听器的结果,不用各自注册。下面给两种实用的实现方式:

方式一:React Context + 全局监听

搞一个Context来统一管理设备类型状态,在顶层组件里注册一次resize监听器,所有子组件直接从Context拿状态就行,不用自己监听。

// MediaQueryContext.tsx
import { createContext, useContext, useEffect, useState, ReactNode } from 'react';

// 这里替换成你实际的断点查询
const desktopQuery = '(min-width: 1024px)'; 
export enum DeviceType {
  Desktop = 'desktop',
  Mobile = 'mobile'
}

interface MediaQueryContextType {
  deviceType: DeviceType;
}

const MediaQueryContext = createContext<MediaQueryContextType | undefined>(undefined);

export const MediaQueryProvider = ({ children }: { children: ReactNode }) => {
  // 初始化设备类型
  const [deviceType, setDeviceType] = useState<DeviceType>(
    window.matchMedia(desktopQuery).matches ? DeviceType.Desktop : DeviceType.Mobile
  );

  useEffect(() => {
    const handleResize = () => {
      const isDesktop = window.matchMedia(desktopQuery).matches;
      setDeviceType(isDesktop ? DeviceType.Desktop : DeviceType.Mobile);
    };

    // 只注册一次全局监听器
    window.addEventListener('resize', handleResize);
    return () => window.removeEventListener('resize', handleResize);
  }, []);

  return (
    <MediaQueryContext.Provider value={{ deviceType }}>
      {children}
    </MediaQueryContext.Provider>
  );
};

// 自定义Hook,从Context取状态
export const useMediaQuery = () => {
  const context = useContext(MediaQueryContext);
  if (!context) {
    throw new Error('useMediaQuery必须在MediaQueryProvider内部使用');
  }
  return context;
};

用的时候,在App最外层包个MediaQueryProvider,然后各个组件直接调用useMediaQuery()就能拿到设备类型,全程只有一个resize监听器。

方式二:模块级变量维护订阅列表

不想用Context的话,也可以在Hook所在的模块里搞个全局的订阅者数组,只注册一次监听器,每次resize时通知所有订阅的组件更新状态。

// useMediaQuery.ts
import { useState, useEffect } from 'react';

const desktopQuery = '(min-width: 1024px)';
export enum DeviceType {
  Desktop = 'desktop',
  Mobile = 'mobile'
}

// 全局变量:存所有组件的状态更新函数
let subscribers = new Set<(deviceType: DeviceType) => void>();
let currentDeviceType: DeviceType;
let isListenerRegistered = false;

// 获取当前设备类型
const getCurrentDevice = (): DeviceType => {
  return window.matchMedia(desktopQuery).matches ? DeviceType.Desktop : DeviceType.Mobile;
};

// 通知所有订阅的组件更新状态
const notifyAll = () => {
  currentDeviceType = getCurrentDevice();
  subscribers.forEach(callback => callback(currentDeviceType));
};

export const useMediaQuery = () => {
  const [deviceType, setDeviceType] = useState<DeviceType>(getCurrentDevice());

  useEffect(() => {
    // 把当前组件的更新函数加入订阅列表
    subscribers.add(setDeviceType);

    // 如果还没注册监听器,就注册一次
    if (!isListenerRegistered) {
      window.addEventListener('resize', notifyAll);
      isListenerRegistered = true;
      currentDeviceType = getCurrentDevice();
    }

    // 组件卸载时移除订阅
    return () => {
      subscribers.delete(setDeviceType);
      // 没人订阅了就移除监听器,省资源
      if (subscribers.size === 0) {
        window.removeEventListener('resize', notifyAll);
        isListenerRegistered = false;
      }
    };
  }, []);

  return { deviceType };
};

这种方式不用改组件结构,直接用Hook就行,全局只有一个resize监听器,而且当没有组件用这个Hook时,还会自动删掉监听器,不会占内存。

额外优化:给resize加防抖

resize事件触发得特别频繁,建议给监听器加个防抖,减少不必要的计算和状态更新。比如在方式一中修改handleResize:

// 先声明定时器变量
let resizeTimeout: NodeJS.Timeout;

const handleResize = () => {
  clearTimeout(resizeTimeout);
  // 100ms内连续触发的话,只执行最后一次
  resizeTimeout = setTimeout(() => {
    const isDesktop = window.matchMedia(desktopQuery).matches;
    setDeviceType(isDesktop ? DeviceType.Desktop : DeviceType.Mobile);
  }, 100);
};

// 清理函数里别忘了清定时器
return () => {
  window.removeEventListener('resize', handleResize);
  clearTimeout(resizeTimeout);
};

这样窗口拖动过程中不会频繁更新状态,性能会更好。

内容的提问来源于stack exchange,提问作者Bolza

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.21 19:15:32