React组件切屏/最小化后批量生成移动圆圈问题求助
我在个人作品集网站里实现了一个BannerSkills组件,功能是每隔数秒生成一个斜向移动的技能圆圈。现在遇到的问题是:当窗口最小化或切换标签页一段时间后返回,组件会一次性生成大量圆圈——目前设置了屏幕最多显示6个,返回时会直接填满这个上限;如果没设限制,甚至会生成无数个。我排查过重渲染相关问题,但没找到解决办法,组件代码如下:
import { useEffect, useRef } from 'react'; import './banner-skills.component.css'; import SKILLS_DATA, { Skill } from '../../data/skills'; const BannerSkills = () => { const CREATE_CIRCLE_MS = 4500; const CREATE_CIRCLE_MS_MOBILE = 7000; const ARRAY_OF_SKILLS: Skill[] = []; const isFirstRender = useRef(true); useEffect(() => { const skillsContainer = document.getElementsByClassName('banner-skills-container')[0]; let minScreenSize = window.screen.width * 0.08; let maxScreenSize = window.screen.width * 0.85; let maxCircles = 6; let prevInitPos = Math.floor(Math.random() * maxScreenSize) - minScreenSize; const createDiagonalCircle = () => { if(window.screen.width <= 500) { minScreenSize = window.screen.width * 0.8; maxCircles = 4; } else { minScreenSize = window.screen.width * 0.08; maxCircles = 6; } if(ARRAY_OF_SKILLS.length >= maxCircles) { return; } const circle = document.createElement('div'); circle.classList.add('skill-circle'); skillsContainer.appendChild(circle); const logo = document.createElement('img'); let randomSkill = SKILLS_DATA[Math.floor(Math.random() * SKILLS_DATA.length)]; while(ARRAY_OF_SKILLS.indexOf(randomSkill) >= 0) { randomSkill = SKILLS_DATA[Math.floor(Math.random() * SKILLS_DATA.length)]; } logo.src = randomSkill.img; ARRAY_OF_SKILLS.push(randomSkill); circle.appendChild(logo); let size = Math.floor(Math.random() * 70) + 70; circle.style.width = size + 'px'; circle.style.height = size + 'px'; let posTop = -150; let posLeft = Math.floor(Math.random() * maxScreenSize) - minScreenSize; while(Math.abs(posLeft - prevInitPos) < (window.screen.width * 0.13)) { posLeft = Math.floor(Math.random() * maxScreenSize) - minScreenSize; } prevInitPos = posLeft; let moveCirclesInterval = setInterval(frame, size - (size * 0.65)); function frame() { if (posTop === 500) { skillsContainer.removeChild(circle); ARRAY_OF_SKILLS.splice(ARRAY_OF_SKILLS.indexOf(randomSkill), 1); clearInterval(moveCirclesInterval); } else { posTop++; posLeft++; circle.style.top = posTop + 'px'; circle.style.left = posLeft + 'px'; circle.style.visibility = 'visible'; } } isFirstRender.current = false; } if(isFirstRender.current === true) { createDiagonalCircle(); if(window.screen.width <= 500) { let spawnCirclesInterval = setInterval(createDiagonalCircle, CREATE_CIRCLE_MS_MOBILE); return () => clearInterval(spawnCirclesInterval); } else { let spawnCirclesInterval = setInterval(createDiagonalCircle, CREATE_CIRCLE_MS); return () => clearInterval(spawnCirclesInterval); } } }, []) return ( <div className="banner-skills-container"></div> ) } export default BannerSkills
问题根源
浏览器在标签页隐藏或窗口最小化时,会降低setInterval的执行频率甚至暂停它,但不会清除定时器。当页面重新激活时,浏览器会把暂停期间本该执行的setInterval回调一次性全部执行,导致短时间内生成大量圆圈。
修复方案
1. 核心改动:用requestAnimationFrame替代setInterval控制生成频率
requestAnimationFrame会自动适配页面活跃状态:页面隐藏时暂停,激活后恢复,不会堆积回调。结合时间戳精准控制生成间隔,避免依赖固定定时器。
2. 优化状态管理
原代码里的ARRAY_OF_SKILLS是组件内全局变量,改用useRef存储,确保状态稳定且不随组件重渲染重置。
修复后的完整代码
import { useEffect, useRef } from 'react'; import './banner-skills.component.css'; import SKILLS_DATA, { Skill } from '../../data/skills'; const BannerSkills = () => { const CREATE_CIRCLE_MS = 4500; const CREATE_CIRCLE_MS_MOBILE = 7000; const skillsArrayRef = useRef<Skill[]>([]); const animationFrameIdRef = useRef<number | null>(null); const lastCreateTimeRef = useRef<number>(Date.now()); useEffect(() => { const skillsContainer = document.querySelector('.banner-skills-container'); if (!skillsContainer) return; // 获取当前设备的生成间隔 const getCreateInterval = () => { return window.innerWidth <= 500 ? CREATE_CIRCLE_MS_MOBILE : CREATE_CIRCLE_MS; }; // 获取当前设备的最大圆圈数量 const getMaxCircles = () => { return window.innerWidth <= 500 ? 4 : 6; }; const createDiagonalCircle = () => { const maxCircles = getMaxCircles(); if (skillsArrayRef.current.length >= maxCircles) return; const circle = document.createElement('div'); circle.classList.add('skill-circle'); skillsContainer.appendChild(circle); const logo = document.createElement('img'); // 随机选择不重复的技能 let randomSkill = SKILLS_DATA[Math.floor(Math.random() * SKILLS_DATA.length)]; while (skillsArrayRef.current.includes(randomSkill)) { randomSkill = SKILLS_DATA[Math.floor(Math.random() * SKILLS_DATA.length)]; } logo.src = randomSkill.img; skillsArrayRef.current.push(randomSkill); circle.appendChild(logo); // 设置圆圈尺寸 const size = Math.floor(Math.random() * 70) + 70; circle.style.width = `${size}px`; circle.style.height = `${size}px`; // 计算初始位置 const minScreenSize = window.innerWidth * (window.innerWidth <= 500 ? 0.8 : 0.08); const maxScreenSize = window.innerWidth * 0.85; let prevInitPos = Math.floor(Math.random() * maxScreenSize) - minScreenSize; let posLeft = Math.floor(Math.random() * maxScreenSize) - minScreenSize; // 避免初始位置过于接近 while (Math.abs(posLeft - prevInitPos) < (window.innerWidth * 0.13)) { posLeft = Math.floor(Math.random() * maxScreenSize) - minScreenSize; } prevInitPos = posLeft; let posTop = -150; // 控制圆圈移动 const moveInterval = setInterval(() => { if (posTop === 500) { skillsContainer.removeChild(circle); const skillIndex = skillsArrayRef.current.indexOf(randomSkill); if (skillIndex !== -1) { skillsArrayRef.current.splice(skillIndex, 1); } clearInterval(moveInterval); } else { posTop++; posLeft++; circle.style.top = `${posTop}px`; circle.style.left = `${posLeft}px`; circle.style.visibility = 'visible'; } }, size - (size * 0.65)); }; // 循环生成圆圈的逻辑 const spawnLoop = () => { const now = Date.now(); const interval = getCreateInterval(); if (now - lastCreateTimeRef.current >= interval) { createDiagonalCircle(); lastCreateTimeRef.current = now; } animationFrameIdRef.current = requestAnimationFrame(spawnLoop); }; // 初始化第一个圆圈 createDiagonalCircle(); // 启动生成循环 animationFrameIdRef.current = requestAnimationFrame(spawnLoop); // 组件卸载时清理资源 return () => { if (animationFrameIdRef.current) { cancelAnimationFrame(animationFrameIdRef.current); } // 清理所有圆圈元素 document.querySelectorAll('.skill-circle').forEach(circle => { skillsContainer?.removeChild(circle); }); skillsArrayRef.current = []; }; }, []); return ( <div className="banner-skills-container"></div> ); }; export default BannerSkills;
关键改动说明
- 用
requestAnimationFrame的循环替代setInterval,利用浏览器帧同步机制,页面隐藏时自动暂停,不会堆积生成任务 - 用
useRef存储技能数组、动画帧ID和上次生成时间,确保状态在组件生命周期内稳定 - 替换
window.screen.width为window.innerWidth,更准确反映当前视窗实际宽度 - 优化组件卸载时的清理逻辑,确保所有动画和残留元素都被清除
内容的提问来源于stack exchange,提问作者Khaled Al Hariri
相关产品推荐
相关产品推荐

