TLDR;
由于这个答案引起了一些关注,我刚刚开发了一个基于它的npm包,让每个人都可以将其作为独立的包/库在他们的项目中使用。因此,如果你想要在React本身或者像Nextjs、Remixjs、Gatsbyjs等服务器端渲染框架/库中立即使用它,你可以将其添加到你的项目中作为一个依赖项:
演示
npm i @smakss/react-scroll-direction
or
yarn add @smakss/react-scroll-direction
点击此处阅读更多。
答案及其描述
这是因为您定义了一个useEffect()
没有任何依赖项,所以您的useEffect()
只会运行一次,并且它从不在y
更改时调用handleNavigation()
。要解决这个问题,您必须将y
添加到您的依赖项数组中,以告诉您的useEffect()
在y
值更改时运行。然后,在您尝试使用window.scrollY
初始化y
的代码中,您应该在useState()
中执行此操作,如下所示:
const [y, setY] = useState(window.scrollY);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => {
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
如果由于某种原因,在那里无法使用
window
,或者您不想在这里这样做,您可以在两个单独的
useEffect()
中完成它。
因此,您的
useEffect()
应该像这样:
useEffect(() => {
setY(window.scrollY);
}, []);
useEffect(() => {
window.addEventListener("scroll", (e) => handleNavigation(e));
return () => {
window.removeEventListener("scroll", (e) => handleNavigation(e));
};
}, [y]);
更新(解决方案)
在自己实施这个解决方案后,我发现应该对这个解决方案进行一些注释。因此,由于handleNavigation()
将直接改变y
的值,我们可以忽略y
作为我们的依赖项,然后将handleNavigation()
作为我们useEffect()
的依赖项添加进去,然后由于这个改变,我们应该对handleNavigation()
进行优化,所以我们应该使用useCallback()
来处理它。然后,最终结果将会是这样的:
const [y, setY] = useState(window.scrollY);
const handleNavigation = useCallback(
e => {
const window = e.currentTarget;
if (y > window.scrollY) {
console.log("scrolling up");
} else if (y < window.scrollY) {
console.log("scrolling down");
}
setY(window.scrollY);
}, [y]
);
useEffect(() => {
setY(window.scrollY);
window.addEventListener("scroll", handleNavigation);
return () => {
window.removeEventListener("scroll", handleNavigation);
};
}, [handleNavigation]);
在@RezaSam的评论之后,我注意到记忆化版本中有一个小错误。在另一个箭头函数中调用
handleNavigation
时,我发现(通过浏览器开发工具的事件监听器选项卡)每个组件都会向
window
注册一个新的事件,这可能会破坏整个功能。
工作演示:
最终优化方案
经过一番思考,我得出结论,在这种情况下,记忆化将帮助我们注册一个单一事件来识别滚动方向。然而,对于在handleNavigation
函数内部进行控制台输出,它并没有完全优化。在当前的实现中,无法打印出所需的控制台信息。
因此,我意识到有一种更好的方法,每次我们想要检查新的状态时,存储上次页面滚动位置。此外,为了消除大量的控制台输出向上滚动和向下滚动,我们应该定义一个阈值(使用防抖动方法)来触发滚动事件的变化。于是,我在网上搜索了一下,找到了这个非常有用的gist。然后,在它的启发下,我实现了一个更简单的版本。
它的效果如下:
const [scrollDir, setScrollDir] = useState("scrolling down");
useEffect(() => {
const threshold = 0;
let lastScrollY = window.pageYOffset;
let ticking = false;
const updateScrollDir = () => {
const scrollY = window.pageYOffset;
if (Math.abs(scrollY - lastScrollY) < threshold) {
ticking = false;
return;
}
setScrollDir(scrollY > lastScrollY ? "scrolling down" : "scrolling up");
lastScrollY = scrollY > 0 ? scrollY : 0;
ticking = false;
};
const onScroll = () => {
if (!ticking) {
window.requestAnimationFrame(updateScrollDir);
ticking = true;
}
};
window.addEventListener("scroll", onScroll);
console.log(scrollDir);
return () => window.removeEventListener("scroll", onScroll);
}, [scrollDir]);
它是如何工作的?
我将从上到下逐个解释每个代码块。
所以我刚刚定义了一个阈值点,初始值为0;每当滚动向上或向下时,都会进行新的计算。如果你不想立即计算新的页面偏移量,可以增加它。
然后,我决定使用
pageYOffset
而不是
scrollY
,因为它在跨浏览器中更可靠。
在
updateScrollDir
函数中,我们将检查是否达到了阈值;然后,如果达到了,我将根据当前和上一个页面偏移量指定滚动方向。
其中最关键的部分是
onScroll
函数。我只是使用
requestAnimationFrame
来确保在滚动后页面完全渲染之后计算新的偏移量。然后,通过
ticking
标志,我们将确保我们的事件监听器回调只在每个
requestAnimationFrame
中运行一次。
最后,我们定义了我们的监听器和清理函数。
scrollDir
状态将包含实际的滚动方向。
工作演示: