React窗口大小计算错误:Chrome设备旋转后尺寸不符的问题与修复
问题分析与修复方案
我之前在Chrome DevTools模拟设备旋转时也碰到过一模一样的问题,尺寸总是不准,咱们来一步步拆解原因和修复方案。
首先先贴出你的示例代码(方便上下文参考):
// ... imports const ComponentName = () => { const [loading, setLoading] = useState(true); const [sizes, setSizes] = useState({ width: 0, height: 0, }); const handleResize = useCallback((e) => { const ww = e === undefined ? window.innerWidth : e.target.innerWidth; const wh = e === undefined ? window.innerHeight : e.target.innerHeight; console.log("ww: ", ww); console.log("wh: ", wh); setSizes({ width: ww, height: wh }); setLoading(false); }, []); useLayoutEffect(() => { window.addEventListener("resize", handleResize); return () => { window.removeEventListener("resize", handleResize); }; }, [handleResize]); useEffect(() => { let mount = true; if (mount && loading) { handleResize(); } return () => { mount = false; }; }, [loading, handleResize]); console.log("sizes: ", sizes.width, " x ", sizes.height); return ( <div> {/* ... render */} </div> ); }; export default ComponentName;
问题到底出在哪?
你碰到的这个情况,主要是Chrome DevTools模拟设备旋转时的时序问题加上代码里的几个小细节导致的:
- resize事件触发太早:设备旋转时,浏览器得重新计算视口尺寸,但resize事件可能在浏览器还没完成视口调整就触发了,拿到的是旋转过程中的过渡尺寸,不是最终的正确值。
- 没有防抖处理:旋转过程中可能会连续触发好几次resize事件,最后一次才是正确的尺寸,但你的代码每次触发都会更新状态,很可能留下最后一次错误的数值。
- 只监听resize不够:移动端设备旋转时,会先触发
orientationchange事件,之后才会完成视口调整并触发resize,只盯着resize可能跟不上这个节奏。 - window.innerWidth的局限性:这个值有时候会被浏览器的UI(比如地址栏、底部导航栏)影响,旋转时这些UI的显示状态变化也会干扰尺寸计算,导致数值不准。
怎么修复?
针对上面的问题,我们可以对代码做几个关键调整:
1. 给resize事件加防抖
确保我们只在resize事件停止触发一段时间后,才去获取最终的尺寸,避开过渡状态的干扰。
2. 同时监听orientationchange事件
在设备旋转时,先通过orientationchange事件触发一次延迟的尺寸检测,确保拿到稳定后的视口数据。
3. 用visualViewport拿更准确的尺寸
window.visualViewport提供了实际可见的视口尺寸,不受浏览器UI变化的影响,更适合移动端场景。
4. 调整初始加载的尺寸获取时机
初始加载时稍微延迟一点再获取尺寸,确保视口已经稳定下来。
修复后的代码如下:
// ... imports import { useState, useCallback, useLayoutEffect, useEffect } from 'react'; // 防抖函数:延迟执行回调,避免频繁触发 const debounce = (func, delay = 200) => { let timeoutId; return (...args) => { clearTimeout(timeoutId); timeoutId = setTimeout(() => func.apply(this, args), delay); }; }; const ComponentName = () => { const [loading, setLoading] = useState(true); const [sizes, setSizes] = useState({ width: 0, height: 0 }); // 调整尺寸获取逻辑,改用visualViewport const updateViewportSize = useCallback(() => { // 使用visualViewport的尺寸,更准确反映实际可见区域,兼容旧浏览器降级到innerWidth const ww = window.visualViewport?.width || window.innerWidth; const wh = window.visualViewport?.height || window.innerHeight; console.log("最终稳定尺寸: ", ww, " x ", wh); setSizes({ width: ww, height: wh }); setLoading(false); }, []); // 给updateViewportSize加上防抖 const debouncedUpdateSize = useCallback(debounce(updateViewportSize), [updateViewportSize]); useLayoutEffect(() => { // 同时监听resize和orientationchange事件 window.addEventListener("resize", debouncedUpdateSize); window.addEventListener("orientationchange", debouncedUpdateSize); // 监听visualViewport自身的resize事件(如果支持的话) window.visualViewport?.addEventListener("resize", debouncedUpdateSize); return () => { // 清理所有监听事件 window.removeEventListener("resize", debouncedUpdateSize); window.removeEventListener("orientationchange", debouncedUpdateSize); window.visualViewport?.removeEventListener("resize", debouncedUpdateSize); }; }, [debouncedUpdateSize]); useEffect(() => { let mount = true; // 初始加载时延迟100ms再获取尺寸,确保视口稳定 const timer = setTimeout(() => { if (mount && loading) { updateViewportSize(); } }, 100); return () => { mount = false; clearTimeout(timer); }; }, [loading, updateViewportSize]); console.log("当前状态尺寸: ", sizes.width, " x ", sizes.height); return ( <div> {/* ... render */} </div> ); }; export default ComponentName;
额外小提示
- 防抖的延迟时间(200ms)可以根据实际情况调整,一般100-300ms都能覆盖浏览器视口调整的时间。
- 如果你需要兼容非常旧的浏览器,可以保留
window.innerWidth作为降级方案,代码里已经做了兼容。 - 在Chrome DevTools里测试时,旋转后稍等片刻,让防抖函数触发,就能拿到正确的尺寸了。
内容的提问来源于stack exchange,提问作者mmstarz
相关产品推荐
相关产品推荐

