React中不使用setTimeout获取DOM元素实现高度计算的方案
React 组件DOM高度获取的正确实现(替代setTimeout延迟方案)
你当前的实现存在几个明显的可靠性问题:
- 组件函数顶层直接执行
document.querySelector:组件每次重渲染都会跑这部分逻辑,此时真实DOM还没完成挂载/更新,很容易拿到null或者旧版本的DOM引用 - 未做清理的裸
setTimeout:每次组件重渲染都会生成新的定时器,既会造成内存泄漏,2秒固定延迟也属于"赌时机"的写法——页面资源加载慢(比如购物车商品接口返回慢、图片没加载完)时DOM还没渲染完成,会拿到错误的高度值;页面加载快时又会平白增加等待时间 - 没有监听动态高度变化:购物车商品异步加载、图片解码、窗口尺寸变化都会导致容器高度改变,只执行一次计算会让后续sticky吸顶逻辑完全失效
正确实现逻辑
- 组件内部渲染的DOM节点(比如你代码里的
.summaryContainer)直接用useRef绑定引用,不需要走querySelector全局查找,性能更高也不会拿错节点 - 所有DOM查询、尺寸计算逻辑放到
useEffect中执行:该钩子触发时React已经把组件渲染结果提交到真实DOM树,能保证拿到已挂载的最新节点 - 用
ResizeObserver监听目标容器的尺寸变化,只要容器高度因为异步内容加载、布局变化发生改变,就自动重新计算相关值,完全不需要靠延迟猜加载时机 - 所有事件监听、观察者都要在effect的清理函数中注销,避免内存泄漏
修正后的完整代码
const OrderSummaryView = () => { const isMobile = useGetBreakpoint() === 'mobile' const { versionIsVariant } = useContext(Abtests.Context) const isFixedScrollEnabled = versionIsVariant('FIXED_SCROLL_ORDER_SUMMARY') && !isMobile // 给组件内的summary容器绑定ref,替代querySelector const summaryRef = useRef(null) const [scrollPosition, setScrollPosition] = useState(0) const [itemContainerHeight, setItemContainerHeight] = useState(0) const [summaryContainerHeight, setSummaryContainerHeight] = useState(0) const [calculatedBreakpoint, setCalculatedBreakpoint] = useState(0) const [mainHeight, setMainHeight] = useState(0) const handleScroll = () => { const position = window.pageYOffset setScrollPosition(position) } // 抽离高度计算逻辑 const calculateDimensions = useCallback(() => { const main = document.querySelector('main[class*="main"]') const cartItemsContainer = document.querySelector('#cartItems') const summaryContainer = summaryRef.current if (!main || !cartItemsContainer || !summaryContainer) return const itemH = cartItemsContainer.clientHeight const summaryH = summaryContainer.clientHeight const mainH = main.clientHeight setItemContainerHeight(itemH) setSummaryContainerHeight(summaryH) setMainHeight(mainH) setCalculatedBreakpoint(itemH - summaryH + 100) }, []) useEffect(() => { window.addEventListener('scroll', handleScroll, { passive: true }) // 初始计算一次,此时DOM已经完成挂载 calculateDimensions() // 监听目标容器的尺寸变化,内容加载/布局变动时自动重算 const resizeObserver = new ResizeObserver(() => { calculateDimensions() }) const main = document.querySelector('main[class*="main"]') const cartItemsContainer = document.querySelector('#cartItems') if (main) resizeObserver.observe(main) if (cartItemsContainer) resizeObserver.observe(cartItemsContainer) if (summaryRef.current) resizeObserver.observe(summaryRef.current) // 监听窗口尺寸变化做兜底 window.addEventListener('resize', calculateDimensions) return () => { window.removeEventListener('scroll', handleScroll) window.removeEventListener('resize', calculateDimensions) resizeObserver.disconnect() } }, [calculateDimensions, isFixedScrollEnabled]) const isSticky = scrollPosition >= 90 && isFixedScrollEnabled && (scrollPosition < calculatedBreakpoint) const isPostSticky = (scrollPosition >= calculatedBreakpoint) && isFixedScrollEnabled const orderSummaryClassname = `orderSummary${isSticky ? `Div__is-sticky` : isPostSticky ? `Div__post-sticky` : 'Div'}` const postStickyTop = `${mainHeight - summaryContainerHeight - 50}px` return ( <div className="summaryContainer" ref={summaryRef}> <div className={orderSummaryClassname} style={isPostSticky ? { top: postStickyTop } : isSticky ? { top: '10px' } : null}> <OrderSummaryTitle title="Order Summary" /> <OrderSummaryBody isFixedScrollEnabled={isFixedScrollEnabled} /> <DesignerCashback /> </div> </div> ) } export default OrderSummaryView
补充说明:如果你的购物车列表包含大量未设固定宽高的图片,可以给
img标签加上width/height属性或者CSS宽高,避免图片加载前后高度跳变;也可以在图片的onLoad事件里触发一次calculateDimensions做兼容。
内容的提问来源于stack exchange,提问作者Berheet
相关产品推荐
相关产品推荐

