React实现聊天室无限滚动时如何避免scrollIntoView触发多余scroll事件
问题背景
我需要为聊天室实现无限滚动功能,要求聊天室加载完成后自动滚动到底部,展示最新的消息。
遇到的问题
我封装了一个无限滚动组件,会预先加载内容直到容器出现滚动溢出,但调用scrollIntoView方法时会触发多余的scroll事件,导致滚动逻辑被误触发。目前想到的备选方案是在scroll回调中校验事件时间戳,给组件设置X毫秒的冷却时间再执行回调逻辑。
现有实现代码
function InfiniteScroll({ children }) { const [readyForScrolling, setReadyForScrolling] = useState(false); useEffect(() => { if (readyForScrolling) { let scrollFunc = () => { // do scroll stuff }; container.addEventListener('scroll', scrollFunc); return () => { container.removeEventListener('scroll', scrollFunc); }; } }); // prefill useEffect(() => { if (checkVarious()) { fetchMore(); } else { setPreFillComplete(true); } }); return children; // pass along props for child as well in real code } function Items(items, preFillComplete, setReadyForScrolling) { const firstItemRef = useRef(); useEffect(() => { if (preFillComplete) { firstItemRef.current.scrollIntoView(); setReadyForScrolling(true); // PROBLEM !! } }); return ( items?.length ? items.map((item, i) => ( <div ref={i == items.length - 1 ? firstItemRef : null}>{item.name}</div> )) ); } function List() { return ( <InfiniteScroll> <Items /> </InfiniteScroll> ); }
复现步骤
- 加载页面,观察控制台不会输出"SCROLLING, SCROLLING ... NOOO"日志。
- 取消注释代码行
firstItem.current.scrollIntoView(); - 重新加载页面,此时控制台会输出上述滚动相关日志。
可行解决方案
方案1:调整滚动事件绑定时机
你当前的逻辑是通过readyForScrolling状态控制滚动事件的绑定,只需要调整操作顺序,等scrollIntoView的执行完全结束后再开启滚动监听即可:
useEffect(() => { if (preFillComplete) { // 先执行滚动操作 firstItemRef.current.scrollIntoView(); // 延迟启用滚动监听,确保本次滚动触发的事件已经结束 setTimeout(() => { setReadyForScrolling(true); }, 0) } }, [preFillComplete]);
这个方案改造成本最低,零额外逻辑侵入,setTimeout延迟0毫秒会把绑定操作推入宏任务队列,等本次同步执行的滚动逻辑、事件触发流程完全结束后再执行,自然不会捕获到这次多余的scroll事件。
方案2:新增编程式滚动的忽略标识
如果需要在预滚动阶段就绑定滚动事件,可以新增一个标识位过滤掉编程式滚动触发的事件:
// InfiniteScroll组件内新增状态 const [ignoreScroll, setIgnoreScroll] = useState(false); useEffect(() => { if (readyForScrolling) { let scrollFunc = () => { // 忽略编程式滚动触发的事件 if (ignoreScroll) return; // 正常执行滚动逻辑 }; container.addEventListener('scroll', scrollFunc); return () => { container.removeEventListener('scroll', scrollFunc); }; } }, [readyForScrolling, ignoreScroll]); // Items组件内修改逻辑 useEffect(() => { if (preFillComplete) { // 滚动前开启忽略标识 setIgnoreScroll(true); firstItemRef.current.scrollIntoView(); // 滚动结束后关闭忽略标识 requestAnimationFrame(() => { setIgnoreScroll(false); }); setReadyForScrolling(true); } }, [preFillComplete]);
这个方案适合需要提前绑定滚动事件的场景,过滤逻辑更可控。
额外优化建议
你当前的所有useEffect都没有填写依赖数组,会导致每轮渲染都重复执行副作用,建议补全依赖数组避免不必要的性能消耗和逻辑bug。
内容的提问来源于stack exchange,提问作者user1849962
相关产品推荐
相关产品推荐

