在 React js 中检测滚动方向

IT技术 javascript reactjs scroll event-handling
2021-04-22 07:04:59

我试图检测滚动事件是向上还是向下,但我找不到解决方案。

import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";

const Navbar = ({ className }) => {
  const [y, setY] = useState(0);

  const handleNavigation = (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);
  };

  useEffect(() => {
    setY(window.scrollY);

    window.addEventListener("scroll", (e) => handleNavigation(e));
  }, []);

  return (
    <nav className={className}>
      <p>
        <i className="fas fa-pizza-slice"></i>Food finder
      </p>
      <ul>
        <li>
          <Link to="/">Home</Link>
        </li>
        <li>
          <Link to="/about">About</Link>
        </li>
      </ul>
    </nav>
  );
};

export default Navbar;

基本上它总是被检测为“down”,因为yinhandleNavigation总是 0。如果我检查 DevTool 中的y状态,状态会更新,但handleNavigation 不会。

任何建议我做错了什么?

谢谢你的帮助

6个回答

这是因为你定义一个useEffect()没有任何依赖关系,所以useEffect()只运行一次,它从来没有要求handleNavigation()y的变化要解决此问题,您需要添加y到您的依赖项数组中,以useEffect()y值发生更改时通知您的运行然后,你需要另一个更改在你的代码,你在哪里试图初始化效果ywindow.scrollY,所以您应该在你做到这一点useState(),如:

const [y, setY] = useState(window.scrollY);

useEffect(() => {
  window.addEventListener("scroll", (e) => handleNavigation(e));

  return () => { // return a cleanup function to unregister our function since its gonna run multiple times
    window.removeEventListener("scroll", (e) => handleNavigation(e));
  };
}, [y]);

如果由于某种原因 window 可能在那里不可用或者您不想在此处执行此操作,则可以在两个单独的useEffect()s 中执行此操作。

所以你的useEffect()s 应该是这样的:

useEffect(() => {
  setY(window.scrollY);
}, []);

useEffect(() => {
  window.addEventListener("scroll", (e) => handleNavigation(e));

  return () => { // return a cleanup function to unregister our function since its gonna run multiple times
    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因此它可能会破坏整个事情。

工作演示:

代码沙盒


最终优化方案

毕竟,在这种情况下,我最终认为memoization将帮助我们注册单个事件,以识别滚动方向,但它在打印控制台时并未完全优化,因为我们在handleNavigation函数内部进行安慰,没有其他方法可以解决在当前实现中打印所需的控制台。

所以,我意识到每次我们想要检查新位置时,都有一种更好的方法来存储最后一页的滚动位置。同样为了摆脱大量的向上滚动向下滚动的安慰,我们应该定义一个阈值(使用去抖动方法)来触发滚动事件更改。所以我只是在网上搜索了一下,最后得到了这个非常有用的要点然后在它的启发下,我实现了一个更简单的版本。

这是它的外观:

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每当滚动向上或向下时,它都会进行新的计算,如果您不想立即计算新的页面偏移量,可以增加它。

  • 然后scrollY我决定使用pageYOffset哪个在交叉浏览中更可靠,而不是使用

  • updateScrollDir函数中,我们将简单地检查是否满足阈值,然后如果满足我将根据当前和上一页的偏移量指定滚动方向。

  • 其中最重要的部分是onScroll功能。我只是用来requestAnimationFrame确保我们在滚动后完全呈现页面后计算新的偏移量。然后使用ticking标志,我们将确保我们只在每个requestAnimationFrame.

  • 最后,我们定义了我们的监听器和我们的清理函数。

  • 然后scrollDir状态将包含实际的滚动方向。

工作演示:

代码沙盒

@RezaSam 感谢您的注意。我修复了它并为了荣誉提到了你。😉 顺便说一句,最后一个解决方案是整洁的,而且完全更好,因为我们onScroll在每个浏览器油漆上都勾选了该功能。
2021-06-05 07:04:59
哇男人!这真是太棒了!非常感谢您的精心工作!
2021-06-10 07:04:59
handleNavigation 作为匿名函数传递,因此 removeListner 将不起作用并且无数的侦听器被添加到窗口
2021-06-12 07:04:59

在 Next.js 中试试这个(如果你在挣扎) -

我用过这个包 - react-use-scroll-direction

import React from 'react'
import { useScrollDirection } from 'react-use-scroll-direction'

export const Window_Scroll_Direction = () => {
const [direction, setDirection] = React.useState(String)
const { isScrollingUp, isScrollingDown } = useScrollDirection()

React.useEffect(() => {
  isScrollingDown && setDirection('down')
  isScrollingUp && setDirection('up')
}, [isScrollingDown, isScrollingUp])

return (
  <>
    <div className="fixed top-0 bg-white">
      {direction === 'down' ? 'Scrolling down' : 'scrolling up'}
    </div>
 </>
 )
}

只是想提出一个简洁的解决方案,它与 habbahans 非常相似,但在我看来看起来更整洁。

let oldScrollY = 0;

const [direction, setDirection] = useState('up');

const controlDirection = () => {
    if(window.scrollY > oldScrollY) {
        setDirection('down');
    } else {
        setDirection('up');
    }
    oldScrollY = window.scrollY;
}

useEffect(() => {
    window.addEventListener('scroll', controlDirection);
    return () => {
        window.removeEventListener('scroll', controlDirection);
    };
},[]);

在这里,您可以访问hidden状态以在代码中执行您希望的操作。

我环顾四周,找不到简单的解决方案,所以我查看了事件本身,并且存在一个“deltaY”,它使一切变得更简单(无需保持最后滚动值的状态)。“deltaY”值显示事件在“y”中的变化(正 deltaY 表示它是向下滚动事件,负 deltaY 表示它是向上滚动事件)。

这是它的工作原理:

componentDidMount() {
    window.addEventListener('scroll', e => this.handleNavigation(e));
}

handleNavigation = (e) => {
    if (e.deltaY > 0) {
        console.log("scrolling down");
    } else if (e.deltaY < 0) {
        console.log("scrolling up");
    }
};
它对我不起作用。deltaY在桌面 Chrome 中未定义。
2021-06-03 07:04:59

这是我的 React 钩子解决方案,useScrollDirection

import { useEffect, useState } from 'react'

export type ScrollDirection = '' | 'up' | 'down'

type HistoryItem = { y: number; t: number }

const historyLength = 32 // Ticks to keep in history.
const historyMaxAge = 512 // History data time-to-live (ms).
const thresholdPixels = 64 // Ignore moves smaller than this.

let lastEvent: Event
let frameRequested: Boolean = false
let history: HistoryItem[] = Array(historyLength)
let pivot: HistoryItem = { t: 0, y: 0 }

export function useScrollDirection({
  scrollingElement,
}: { scrollingElement?: HTMLElement | null } = {}): ScrollDirection {
  const [scrollDirection, setScrollDirection] = useState<ScrollDirection>('')

  useEffect(() => {
    const element: Element | null =
      scrollingElement !== undefined ? scrollingElement : document.scrollingElement
    if (!element) return

    const tick = () => {
      if (!lastEvent) return
      frameRequested = false

      let y = element.scrollTop
      const t = lastEvent.timeStamp
      const furthest = scrollDirection === 'down' ? Math.max : Math.min

      // Apply bounds to handle rubber banding
      const yMax = element.scrollHeight - element.clientHeight
      y = Math.max(0, y)
      y = Math.min(yMax, y)

      // Update history
      history.unshift({ t, y })
      history.pop()

      // Are we continuing in the same direction?
      if (y === furthest(pivot.y, y)) {
        // Update "high-water mark" for current direction
        pivot = { t, y }
        return
      }
      // else we have backed off high-water mark

      // Apply max age to find current reference point
      const cutoffTime = t - historyMaxAge
      if (cutoffTime > pivot.t) {
        pivot.y = y
        history.filter(Boolean).forEach(({ y, t }) => {
          if (t > cutoffTime) pivot.y = furthest(pivot.y, y)
        })
      }

      // Have we exceeded threshold?
      if (Math.abs(y - pivot.y) > thresholdPixels) {
        pivot = { t, y }
        setScrollDirection(scrollDirection === 'down' ? 'up' : 'down')
      }
    }

    const onScroll = (event: Event) => {
      lastEvent = event
      if (!frameRequested) {
        requestAnimationFrame(tick)
        frameRequested = true
      }
    }

    element.addEventListener('scroll', onScroll)
    return () => element.removeEventListener('scroll', onScroll)
  }, [scrollDirection, scrollingElement])

  return scrollDirection
}

用法:

const [scrollingElement, setScrollingElement] = useState<HTMLElement | null>(null)
const ref = useCallback(node => setScrollingElement(node), [setScrollingElement])
const scrollDirection = useScrollDirection({ scrollingElement })

<ScrollingContainer {...{ ref }}>
  <Header {...{ scrollDirection }}>
</ScrollingContainer>

基于https://github.com/pwfisher/scroll-intenthttps://github.com/dollarshaveclub/scrolldir也在这里移植到 React:https : //github.com/AnakinYuen/scroll-direction