在React函数组件开发过程中,useEffect是处理副作用操作的核心钩子,然而在无限滚动这类需要持续监听滚动状态、频繁触发数据请求的场景中,不少开发者会遇到useEffect重复执行、组件卸载后仍在发起请求、滚动触发过于密集导致性能损耗等问题。这些问题的根源大多是对useEffect的执行机制、依赖规则以及清理逻辑理解不够透彻。

useEffect的基础执行逻辑
useEffect会在组件渲染完成后异步执行,其执行时机和依赖数组的配置直接相关,具体规则如下:
- 不传入依赖数组时,每次组件渲染完成后都会执行副作用函数
- 依赖数组为空数组时,仅在组件首次渲染完成后执行一次
- 依赖数组包含具体变量时,只有当这些变量发生改变时,才会重新执行副作用函数
同时useEffect可以返回一个清理函数,这个清理函数会在组件卸载时执行,也会在下次副作用函数执行前先执行,用来清除上一次副作用产生的残留影响。
无限滚动场景下的常见问题
实现无限滚动通常需要监听容器的滚动事件,判断滚动位置是否到达底部,到达后触发数据请求加载更多内容。很多初学者的实现方式如下:
import { useEffect, useState, useRef } from 'react';
function InfiniteScrollList() {
const [list, setList] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const containerRef = useRef(null);
// 问题实现:未正确配置依赖和清理逻辑
useEffect(() => {
const container = containerRef.current;
const handleScroll = () => {
if (loading) return;
const { scrollTop, clientHeight, scrollHeight } = container;
// 距离底部小于50px时触发加载
if (scrollHeight - scrollTop - clientHeight < 50) {
setPage(prev => prev + 1);
}
};
container.addEventListener('scroll', handleScroll);
}, []);
// 监听page变化请求数据
useEffect(() => {
const fetchData = async () => {
setLoading(true);
// 模拟接口请求
const newData = await new Promise(resolve => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => `列表项 ${list.length + i + 1}`));
}, 1000);
});
setList(prev => [...prev, ...newData]);
setLoading(false);
};
fetchData();
}, [page]);
return (
<div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
{list.map((item, index) => (
<div key={index} style={{ height: '60px', lineHeight: '60px', borderBottom: '1px solid #eee' }}>
{item}
</div>
))}
{loading && <div style={{ textAlign: 'center', padding: '10px' }}>加载中...</div>}
</div>
);
}这段实现存在两个明显问题:
- 第一个useEffect没有返回清理函数,组件卸载或者重新绑定事件时,旧的滚动监听不会被移除,会导致事件重复绑定,甚至组件卸载后仍然触发监听逻辑
- page变化时每次都会发起新的数据请求,没有做请求去重和防抖处理,快速滚动时会连续触发多次page更新,导致重复请求相同数据
针对性优化方案
1. 完善事件监听的清理逻辑
在useEffect中绑定滚动事件后,一定要在清理函数中移除对应的事件监听,避免内存泄漏和重复触发:
useEffect(() => {
const container = containerRef.current;
const handleScroll = () => {
if (loading) return;
const { scrollTop, clientHeight, scrollHeight } = container;
if (scrollHeight - scrollTop - clientHeight < 50) {
setPage(prev => prev + 1);
}
};
container.addEventListener('scroll', handleScroll);
// 返回清理函数移除事件监听
return () => {
container.removeEventListener('scroll', handleScroll);
};
}, [loading]); // 依赖loading,避免loading状态变化时监听逻辑异常2. 添加滚动防抖与请求锁
滚动事件触发频率极高,需要添加防抖逻辑减少判断次数,同时用请求锁避免重复请求:
import { useEffect, useState, useRef } from 'react';
function OptimizedInfiniteScrollList() {
const [list, setList] = useState([]);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const containerRef = useRef(null);
const timerRef = useRef(null); // 防抖定时器引用
useEffect(() => {
const container = containerRef.current;
const handleScroll = () => {
// 防抖处理,100ms内只判断一次
if (timerRef.current) clearTimeout(timerRef.current);
timerRef.current = setTimeout(() => {
if (loading) return;
const { scrollTop, clientHeight, scrollHeight } = container;
if (scrollHeight - scrollTop - clientHeight < 50) {
setPage(prev => prev + 1);
}
}, 100);
};
container.addEventListener('scroll', handleScroll);
return () => {
container.removeEventListener('scroll', handleScroll);
// 组件卸载时清除定时器
if (timerRef.current) clearTimeout(timerRef.current);
};
}, [loading]);
useEffect(() => {
// 用ref记录当前请求状态,避免闭包导致的状态异常
const fetchData = async () => {
if (loading) return;
setLoading(true);
try {
const newData = await new Promise(resolve => {
setTimeout(() => {
resolve(Array.from({ length: 10 }, (_, i) => `列表项 ${list.length + i + 1}`));
}, 1000);
});
setList(prev => [...prev, ...newData]);
} finally {
setLoading(false);
}
};
fetchData();
}, [page]);
return (
<div ref={containerRef} style={{ height: '400px', overflow: 'auto' }}>
{list.map((item, index) => (
<div key={index} style={{ height: '60px', lineHeight: '60px', borderBottom: '1px solid #eee' }}>
{item}
</div>
))}
{loading && <div style={{ textAlign: 'center', padding: '10px' }}>加载中...</div>}
</div>
);
}3. 依赖数组的正确配置
useEffect的依赖数组需要包含所有在副作用内部使用到的、会发生变化的状态或属性,避免闭包陷阱。比如上面的滚动监听依赖了loading状态,就需要把loading加入依赖数组,否则可能会出现loading状态更新后,监听函数内部仍然使用旧状态的问题。
总结
在无限滚动场景中使用useEffect,核心是要理清副作用的执行时机和清理规则,做好事件监听的移除、滚动事件的防抖处理以及请求的状态控制。同时要根据实际使用的状态合理配置依赖数组,避免闭包导致的逻辑异常。掌握这些要点后,不仅能解决无限滚动场景的问题,也能在其他需要用到useEffect的场景中避免常见的性能问题和逻辑bug。