React中三列循环滚动的setInterval异常问题排查与解决
三列循环滚动布局问题解决
问题描述
实现一个三列布局的React组件,要求三列通过动画循环滚动:滚动至底部后返回顶部,到达顶部后再次向下滚动。当前实现中列能正常滚动到底部,但无法返回顶部。相关代码如下:
import React, { useState, useEffect } from "react"; import { map } from "lodash"; const column1InitialYPosition = -300; const column2InitialYPosition = -150; const column3InitialYPosition = 0; const intervalTime = 500; const imageUrl = "https://images.freeimages.com/variants/jzAZ1zYpxh11gSbgYPMheWun/f4a36f6589a0e50e702740b15352bc00e4bfaf6f58bd4db850e167794d05993d"; const column1Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl]; const column2Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl]; const column3Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl]; const isElementInViewport = el => { let r, html; if (!el || 1 !== el.nodeType) { return false; } html = document.documentElement; r = el.getBoundingClientRect(); return !!r && r.bottom >= 0 && r.top <= html.clientHeight; }; export default function App() { const [column1YPosition, setColumn1YPosition] = useState( column1InitialYPosition ); const [column2YPosition, setColumn2YPosition] = useState( column2InitialYPosition ); const [column3YPosition, setColumn3YPosition] = useState( column3InitialYPosition ); let intervalScroll; useEffect(() => { setTimeout(() => startScrollInterval(), 3000); return () => removeScrollToBottomInterval(); }, []); const calculateNewPosition = ( currentPosition, maxPosition, minPosition, bottomReached, topReached ) => { let newPosition = bottomReached ? currentPosition + 100 : currentPosition - 100; newPosition = -1 * newPosition > maxPosition ? -maxPosition : newPosition; newPosition = newPosition > minPosition ? minPosition - 1 : newPosition; return newPosition; }; const startScrollInterval = () => { const screenHeight = window ? window?.innerHeight : 0; const column1Bottom = document.getElementById("column-1-bottom"); const column1Top = document.getElementById("column-1-top"); const column1 = document.getElementById("column-1"); const column1MaxTop = column1 ? column1?.scrollHeight - screenHeight : 0; const column2Bottom = document.getElementById("column-2-bottom"); const column2Top = document.getElementById("column-2-top"); const column2 = document.getElementById("column-2"); const column2MaxTop = column2 ? column2?.scrollHeight - screenHeight : 0; const column3Bottom = document.getElementById("column-3-bottom"); const column3Top = document.getElementById("column-3-top"); const column3 = document.getElementById("column-3"); const column3MaxTop = column3 ? column3?.scrollHeight - screenHeight : 0; const intervalHandler = () => { const column1BottomReached = isElementInViewport(column1Bottom); const column1TopReached = isElementInViewport(column1Top); const column2BottomReached = isElementInViewport(column2Bottom); const column2TopReached = isElementInViewport(column2Top); const column3BottomReached = isElementInViewport(column3Bottom); const column3TopReached = isElementInViewport(column3Top); setColumn1YPosition(column1YPosition => { return calculateNewPosition( column1YPosition, column1MaxTop, column1InitialYPosition, column1BottomReached, column1TopReached ); }); setColumn2YPosition(column2YPosition => { return calculateNewPosition( column2YPosition, column2MaxTop, column2InitialYPosition, column2BottomReached, column2TopReached ); }); setColumn3YPosition(column3YPosition => { return calculateNewPosition( column3YPosition, column3MaxTop, column3InitialYPosition, column3BottomReached, column3TopReached ); }); }; intervalScroll = setInterval(intervalHandler, intervalTime); }; const removeScrollToBottomInterval = () => { clearInterval(intervalScroll); intervalScroll = null; }; return ( <div className="relative w-full h-screen overflow-hidden"> <div className="grid grid-cols-3 px-24 gap-24"> <div className="relative overflow-hidden h-screen no-scrollbar"> <div id="column-1" className="absolute inset-0 grid grid-cols-1 gap-24" style={{ transition: "all 6s linear", top: column1YPosition }} > {map(column1Images, (image, i) => { const isFirst = i === 0; const isLast = i === column1Images?.length - 1; return ( <div key={`column-1-vehicle-${i}`} className="relative w-full"> <img src={image} alt="Vehicle" className="w-full h-80" /> {isFirst && ( <div id="column-1-top" className="absolute opacity-1 w-full h-20" style={{ top: -1 * column1InitialYPosition }} /> )} {isLast && ( <div id="column-1-bottom" className="absolute bottom-0 opacity-1 w-full h-20" /> )} </div> ); })} </div> </div> <div className="relative overflow-hidden h-screen no-scrollbar"> <div id="column-2" className="absolute inset-0 grid grid-cols-1 gap-24" style={{ transition: "all 5s linear", top: column2YPosition }} > {map(column2Images, (image, i) => { const isFirst = i === 0; const isLast = i === column2Images?.length - 1; return ( <div key={`column-2-vehicle-${i}`} className="relative w-full"> <img src={image} alt="Vehicle" className="w-full h-80" /> {isFirst && ( <div id="column-2-top" className="absolute opacity-1 w-full h-20" style={{ top: -1 * column2InitialYPosition }} /> )} {isLast && ( <div id="column-2-bottom" className="absolute bottom-0 opacity-1 w-full h-20" /> )} </div> ); })} </div> </div> <div className="relative overflow-hidden h-screen no-scrollbar"> <div id="column-3" className="absolute inset-0 grid grid-cols-1 gap-24" style={{ transition: "all 6s linear", top: column3YPosition }} > {map(column3Images, (image, i) => { const isFirst = i === 0; const isLast = i === column3Images?.length - 1; return ( <div key={`column-3-vehicle-${i}`} className="relative w-full"> <img src={image} alt="Vehicle" className="w-full h-80" /> {isFirst && ( <div id="column-3-top" className="absolute opacity-1 w-full h-20" style={{ top: -1 * column3InitialYPosition }} /> )} {isLast && ( <div id="column-3-bottom" className="absolute bottom-0 opacity-1 w-full h-20" /> )} </div> ); })} </div> </div> </div> </div> ); }
问题分析与修复
核心问题
- 滚动方向切换逻辑失效:仅依赖视口检测元素的可见性判断边界,但顶部检测元素设置了固定偏移,导致无法正确触发顶部到达的判断。
- 边界计算逻辑混乱:
calculateNewPosition中的边界限制公式错误,到达底部后无法正确计算向上滚动的位置。 - 缺乏持久方向状态:没有为每列维护滚动方向状态,单次检测容易出现判断失误。
修复方案
1. 为每列添加滚动方向状态
新增状态变量记录每列当前滚动方向,确保方向切换稳定:
const [column1Direction, setColumn1Direction] = useState('down'); const [column2Direction, setColumn2Direction] = useState('down'); const [column3Direction, setColumn3Direction] = useState('down');
2. 修正顶部检测元素定位
移除顶部检测元素的固定偏移,让它固定在列的顶部:
{isFirst && ( <div id="column-1-top" className="absolute top-0 opacity-1 w-full h-20" /> )}
3. 重写位置计算与边界判断逻辑
简化位置计算逻辑,结合方向状态和实际滚动边界判断,到达边界时自动切换方向:
const calculateNewPosition = ( currentPosition, maxScrollDistance, initialPosition, direction, setDirection ) => { let newPosition; // 向下滚动:top值减小(更负) if (direction === 'down') { newPosition = currentPosition - 100; // 到达底部:top的绝对值 >= 最大滚动距离 if (-newPosition >= maxScrollDistance) { newPosition = -maxScrollDistance; setDirection('up'); } } else { // 向上滚动:top值增大 newPosition = currentPosition + 100; // 到达顶部:回到初始位置 if (newPosition >= initialPosition) { newPosition = initialPosition; setDirection('down'); } } return newPosition; };
4. 更新定时器状态逻辑
在定时器回调中使用方向状态计算新位置:
setColumn1YPosition(prev => calculateNewPosition( prev, column1MaxTop, column1InitialYPosition, column1Direction, setColumn1Direction ) );
完整修改后的代码
import React, { useState, useEffect } from "react"; import { map } from "lodash"; const column1InitialYPosition = -300; const column2InitialYPosition = -150; const column3InitialYPosition = 0; const intervalTime = 500; const imageUrl = "https://images.freeimages.com/variants/jzAZ1zYpxh11gSbgYPMheWun/f4a36f6589a0e50e702740b15352bc00e4bfaf6f58bd4db850e167794d05993d"; const column1Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl]; const column2Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl]; const column3Images = [imageUrl, imageUrl, imageUrl, imageUrl, imageUrl]; export default function App() { const [column1YPosition, setColumn1YPosition] = useState(column1InitialYPosition); const [column2YPosition, setColumn2YPosition] = useState(column2InitialYPosition); const [column3YPosition, setColumn3YPosition] = useState(column3InitialYPosition); // 新增滚动方向状态 const [column1Direction, setColumn1Direction] = useState('down'); const [column2Direction, setColumn2Direction] = useState('down'); const [column3Direction, setColumn3Direction] = useState('down'); let intervalScroll; useEffect(() => { setTimeout(() => startScrollInterval(), 3000); return () => removeScrollToBottomInterval(); }, []); const calculateNewPosition = ( currentPosition, maxScrollDistance, initialPosition, direction, setDirection ) => { let newPosition; if (direction === 'down') { newPosition = currentPosition - 100; // 到达底部:top的绝对值 >= 最大滚动距离 if (-newPosition >= maxScrollDistance) { newPosition = -maxScrollDistance; setDirection('up'); } } else { newPosition = currentPosition + 100; // 到达顶部:回到初始位置 if (newPosition >= initialPosition) { newPosition = initialPosition; setDirection('down'); } } return newPosition; }; const startScrollInterval = () => { const screenHeight = window?.innerHeight || 0; const column1 = document.getElementById("column-1"); const column1MaxTop = column1 ? column1.scrollHeight - screenHeight : 0; const column2 = document.getElementById("column-2"); const column2MaxTop = column2 ? column2.scrollHeight - screenHeight : 0; const column3 = document.getElementById("column-3"); const column3MaxTop = column3 ? column3.scrollHeight - screenHeight : 0; const intervalHandler = () => { setColumn1YPosition(prev => calculateNewPosition( prev, column1MaxTop, column1InitialYPosition, column1Direction, setColumn1Direction ) ); setColumn2YPosition(prev => calculateNewPosition( prev, column2MaxTop, column2InitialYPosition, column2Direction, setColumn2Direction ) ); setColumn3YPosition(prev => calculateNewPosition( prev, column3MaxTop, column3InitialYPosition, column3Direction, setColumn3Direction ) ); }; intervalScroll = setInterval(intervalHandler, intervalTime); }; const removeScrollToBottomInterval = () => { clearInterval(intervalScroll); intervalScroll = null; }; return ( <div className="relative w-full h-screen overflow-hidden"> <div className="grid grid-cols-3 px-24 gap-24"> <div className="relative overflow-hidden h-screen no-scrollbar"> <div id="column-1" className="absolute inset-0 grid grid-cols-1 gap-24" style={{ transition: "all 6s linear", top: column1YPosition }} > {map(column1Images, (image, i) => { const isFirst = i === 0; const isLast = i === column1Images.length - 1; return ( <div key={`column-1-vehicle-${i}`} className="relative w-full"> <img src={image} alt="Vehicle" className="w-full h-80" /> {isFirst && ( <div id="column-1-top" className="absolute top-0 opacity-1 w-full h-20" /> )} {isLast && ( <div id="column-1-bottom" className="absolute bottom-0 opacity-1 w-full h-20" /> )} </div> ); })} </div> </div> <div className="relative overflow-hidden h-screen no-scrollbar"> <div id="column-2" className="absolute inset-0 grid grid-cols-1 gap-24" style={{ transition: "all 5s linear", top: column2YPosition }} > {map(column2Images, (image, i) => { const isFirst = i === 0
相关产品推荐
相关产品推荐

