Next.js/React中实现类似elsner.com的分段滚动功能问题
Next.js分段滚动操作时连续跳转多区块的问题修复
我在Next.js应用中尝试实现分段滚动功能,通过window.addEventListener监听滚轮、按键等事件来控制滚动。目前按键控制的效果符合预期,但使用滚轮或触摸滑动时,会出现连续跳转多个区块的问题,以下是我的初始实现代码:
const options = { root: null, rootMargin: '0px', threshold: 1.0, }; const homeref = useRef(null); const mainref = useRef(null); const aboutref = useRef(null); const servicesref = useRef(null); const solutionsref = useRef(null); const idoref = useRef(null); const contactref = useRef(null); const [mainactive, setmainactive] = useState('home'); const scrollto = (id) => { scroller.scrollTo(id, { duration: 1000, delay: 0, smooth: 'easeInOutQuart', }); }; const itemset = (id, scroll) => { setmainactive(id); scrollto(scroll); return; }; const handleScroll = (direction) => { const sections = ['home', 'about', 'services', 'ido', 'solutions', 'contact']; const currentIndex = sections.indexOf(mainactive); let newIndex = currentIndex; if (direction === 'down') { if (mainactive === 'about') { const aboutBottom = aboutref.current.getBoundingClientRect().bottom; if (aboutBottom <= window.innerHeight) { newIndex = currentIndex < sections.length - 1 ? currentIndex + 1 : currentIndex; } } else { newIndex = currentIndex < sections.length - 1 ? currentIndex + 1 : currentIndex; } } else if (direction === 'up') { newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex; } const nextSection = sections[newIndex]; if (nextSection !== mainactive) { setmainactive(nextSection); scrollto(nextSection); } }; useEffect(() => { const handleKeyDown = (event) => { const wheeldown = event.wheelDeltaY < 0 || event.deltaY > 0; const wheelup = event.wheelDeltaY > 0 || event.deltaY < 0; if (event.key === 'ArrowDown' || wheeldown || event.key === 'ArrowRight') { if ((mainactive === 'home' && window.scrollY > 0) || mainactive === 'about') { setmainactive('about'); handleScroll('down'); } else if (mainactive === 'about') { handleScroll('down'); } else if (mainactive === 'services') { handleScroll('down'); } else if (mainactive === 'ido') { handleScroll('down'); } } else if (event.key === 'ArrowUp' || wheelup || 'ArrowLeft') { if (mainactive === 'services' || mainactive === 'ido' || mainactive == 'solutions') { handleScroll('up'); } else if (mainactive === 'about') { setTimeout(() => { setmainactive('home'); }, 1000); scrollto('home'); } } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('wheel', handleKeyDown); window.addEventListener('touchmove', handleKeyDown, { passive: false }); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('wheel', handleKeyDown); }; }, [mainactive]);
问题原因
滚轮和触摸滑动事件会连续触发多次(比如滚轮滚动一次会触发多个wheel事件),每次触发都会执行一次滚动跳转逻辑,导致连续跳过多个区块。此外,原代码中存在条件判断重复、ArrowLeft逻辑错误等问题,也会加剧异常行为。
修复方案
- 添加滚动锁定状态,在滚动动画过程中禁止触发新的滚动请求
- 修正事件处理中的逻辑错误
- 对滚轮/触摸事件添加防抖或直接阻止默认行为(在锁定状态下)
修改后的完整代码
import { useState, useRef, useEffect } from 'react'; const options = { root: null, rootMargin: '0px', threshold: 1.0, }; const homeref = useRef(null); const mainref = useRef(null); const aboutref = useRef(null); const servicesref = useRef(null); const solutionsref = useRef(null); const idoref = useRef(null); const contactref = useRef(null); const [mainactive, setmainactive] = useState('home'); // 新增:标记是否正在执行滚动动画 const [isScrolling, setIsScrolling] = useState(false); const scrollto = (id) => { setIsScrolling(true); scroller.scrollTo(id, { duration: 1000, delay: 0, smooth: 'easeInOutQuart', // 滚动结束后解锁 onComplete: () => { setIsScrolling(false); } }); }; const itemset = (id, scroll) => { setmainactive(id); scrollto(scroll); }; const handleScroll = (direction) => { const sections = ['home', 'about', 'services', 'ido', 'solutions', 'contact']; const currentIndex = sections.indexOf(mainactive); let newIndex = currentIndex; if (direction === 'down') { if (mainactive === 'about') { const aboutBottom = aboutref.current?.getBoundingClientRect().bottom; if (aboutBottom <= window.innerHeight) { newIndex = currentIndex < sections.length - 1 ? currentIndex + 1 : currentIndex; } } else { newIndex = currentIndex < sections.length - 1 ? currentIndex + 1 : currentIndex; } } else if (direction === 'up') { newIndex = currentIndex > 0 ? currentIndex - 1 : currentIndex; } const nextSection = sections[newIndex]; if (nextSection !== mainactive && !isScrolling) { setmainactive(nextSection); scrollto(nextSection); } }; useEffect(() => { const handleKeyDown = (event) => { // 如果正在滚动,直接阻止默认行为并返回 if (isScrolling) { event.preventDefault(); return; } const wheeldown = event.type === 'wheel' && (event.wheelDeltaY < 0 || event.deltaY > 0); const wheelup = event.type === 'wheel' && (event.wheelDeltaY > 0 || event.deltaY < 0); const touchDown = event.type === 'touchmove' && event.touches[0].clientY < event.changedTouches[0].clientY; const touchUp = event.type === 'touchmove' && event.touches[0].clientY > event.changedTouches[0].clientY; // 处理向下滚动/按键 if (event.key === 'ArrowDown' || wheeldown || event.key === 'ArrowRight' || touchDown) { switch(mainactive) { case 'home': if (window.scrollY > 0) { handleScroll('down'); } break; case 'about': case 'services': case 'ido': case 'solutions': handleScroll('down'); break; default: break; } // 阻止滚轮/触摸的默认滚动行为 if (event.type === 'wheel' || event.type === 'touchmove') { event.preventDefault(); } } // 处理向上滚动/按键 else if (event.key === 'ArrowUp' || wheelup || event.key === 'ArrowLeft' || touchUp) { switch(mainactive) { case 'about': setIsScrolling(true); scrollto('home'); setTimeout(() => { setmainactive('home'); setIsScrolling(false); }, 1000); break; case 'services': case 'ido': case 'solutions': case 'contact': handleScroll('up'); break; default: break; } if (event.type === 'wheel' || event.type === 'touchmove') { event.preventDefault(); } } }; window.addEventListener('keydown', handleKeyDown); window.addEventListener('wheel', handleKeyDown, { passive: false }); window.addEventListener('touchmove', handleKeyDown, { passive: false }); return () => { window.removeEventListener('keydown', handleKeyDown); window.removeEventListener('wheel', handleKeyDown); window.removeEventListener('touchmove', handleKeyDown); }; }, [mainactive, isScrolling]);
关键修改点说明
- 滚动锁定状态
isScrolling:在开始滚动时设为true,滚动动画完成后通过onComplete回调重置为false,期间阻止任何新的滚动请求。 - 修正事件判断逻辑:区分事件类型(按键/滚轮/触摸),修复原代码中
ArrowLeft的逻辑错误(原代码中|| 'ArrowLeft'会始终为真)。 - 阻止默认行为:对滚轮和触摸事件调用
event.preventDefault(),避免浏览器原生滚动和自定义滚动冲突。 - 简化条件判断:用
switch代替重复的if-else,让逻辑更清晰。
内容的提问来源于stack exchange,提问作者user23239345
相关产品推荐
相关产品推荐

