useEffect实现viewport自定义Hook内存泄漏问题排查
React useEffect自定义Hook内存泄漏警告排查
问题根因
控制台抛出的状态更新警告,本质是组件卸载后仍有异步任务执行setState,核心问题出在防抖逻辑的清理缺失:
- 现有
debounce实现所有执行逻辑都通过setTimeout延迟触发,包括挂载时主动调用的初始化逻辑,不会立刻执行目标函数 useWindowSize的effect清理逻辑仅移除了resize事件监听,没有清除防抖函数内部排队的定时器- 若组件在防抖的300ms等待窗口内卸载(比如路由快速跳转、条件渲染移除组件),定时器到期后依然会执行
setWindowSize更新已卸载组件的状态,直接触发警告 - 额外问题:首次加载时需要等300ms才能拿到窗口尺寸,首屏会出现短暂的viewport值为空的情况
修复步骤
1. 改造debounce工具函数,增加取消能力
给防抖函数增加cancel方法,用于主动清除待执行的定时器,这是通用防抖工具的标准能力:
// helpers/helpers 中debounce修改后代码 export function debounce<U = unknown, V = void>( fn: (args: U) => V, wait: number ) { let timer: ReturnType<typeof setTimeout> | null = null const debounced = (args: U): Promise<V> => new Promise((resolve) => { if (timer) clearTimeout(timer) timer = setTimeout(() => { resolve(fn(args)) timer = null }, wait) }) // 新增取消方法,清理待执行任务 debounced.cancel = () => { if (timer) { clearTimeout(timer) timer = null } } return debounced }
2. 修复useWindowSize逻辑
初始化直接读取window尺寸避免首屏等待,卸载时调用cancel方法清除待执行的防抖任务:
import { useState, useEffect } from "react" import { debounce } from "helpers/helpers" export function useWindowSize() { // 初始化直接取window值,兼容SSR场景判断 const [windowSize, setWindowSize] = useState({ width: typeof window !== 'undefined' ? window.innerWidth : undefined, height: typeof window !== 'undefined' ? window.innerHeight : undefined, }) useEffect(() => { function handleResize() { setWindowSize({ width: window.innerWidth, height: window.innerHeight, }) const vh = (window.top || window).innerHeight * 0.01 document.documentElement.style.setProperty("--vh", `${vh}px`) } const debouncedHandleResize = debounce(handleResize, 300) window.addEventListener("resize", debouncedHandleResize) // 初始化直接执行一次,不走防抖延迟 handleResize() return () => { window.removeEventListener("resize", debouncedHandleResize) // 卸载时清除所有待执行的防抖回调 debouncedHandleResize.cancel() } }, []) return windowSize }
3. 优化useViewport实现(可选,从根源减少多余setState)
现有useViewport单独维护viewport状态属于多余更新,直接根据windowWidth计算返回值即可,减少一次状态流转开销:
import { breakpoints } from "helpers/breakpoints" import { useWindowSize } from "hooks/useWindowSize" import { Viewport } from "./types/useViewportTypes" const useViewport = () => { const windowWidth = useWindowSize().width const width = windowWidth ?? (typeof window !== 'undefined' ? window.innerWidth : 0) if (!width) return undefined return { isZeroAndUp: width >= 0, isMobileAndUp: width >= breakpoints.mobileAndUp, isTabletAndUp: width >= breakpoints.tabletAndUp, isDesktopAndUp: width >= breakpoints.desktopAndUp, isLargeDesktopAndUp: width >= breakpoints.largeDesktopAndUp, isMaxWidthAndUp: width >= breakpoints.maxWidthAndUp, } as Viewport } export default useViewport
验证结果
修复后快速切换路由、频繁挂载卸载使用该Hook的组件,控制台不会再抛出内存泄漏警告,首屏也不会出现viewport值为空的短暂异常。
内容的提问来源于stack exchange,提问作者Siouxx
相关产品推荐
相关产品推荐

