发布于 2026-01-05 1 阅读
0

使用 React Hooks 跟踪滚动位置 use-scroll-position

使用 React Hooks 跟踪滚动位置

use-scroll-position

好消息,各位!

今天我们将学习如何使用JavaScript滚动事件监听器来跟踪浏览器滚动位置hooks——这是React一个不算太老的新功能。

我为什么需要它?

跟踪视口/元素滚动位置非常有用,而且在大多数情况下,这是为您的 Web 项目添加炫酷动态功能(例如:)的唯一方法。

  • 滚动时可隐藏/显示的动态导航栏。
  • 滚动时保持在原位的粘性元素。
  • 在页面滚动过程中,动态弹出窗口和用户消息会在特定位置显示。
  • ETC。

这里有一些例子

实时编辑功能CodeSandBox

首先,我注意到大多数情况下,滚动监听器会执行非常耗时的工作,例如查询DOM元素、读取高度/宽度等等。

React上下文来看,这会导致很多不必要的重新渲染,并且会产生副作用,严重影响应用程序的整体性能!

在本文中,我将尝试通过实现一个useScrollPosition React注重性能的钩子来解决上述问题!

那么,出发吧!

最终版本的钩子看起来会像这样:



import { useRef, useLayoutEffect } from 'react'

const isBrowser = typeof window !== `undefined`

function getScrollPosition({ element, useWindow }) {
  if (!isBrowser) return { x: 0, y: 0 }

  const target = element ? element.current : document.body
  const position = target.getBoundingClientRect()

  return useWindow
    ? { x: window.scrollX, y: window.scrollY }
    : { x: position.left, y: position.top }
}

export function useScrollPosition(effect, deps, element, useWindow, wait) {
  const position = useRef(getScrollPosition({ useWindow }))

  let throttleTimeout = null

  const callBack = () => {
    const currPos = getScrollPosition({ element, useWindow })
    effect({ prevPos: position.current, currPos })
    position.current = currPos
    throttleTimeout = null
  }

  useLayoutEffect(() => {
    const handleScroll = () => {
      if (wait) {
        if (throttleTimeout === null) {
          throttleTimeout = setTimeout(callBack, wait)
        }
      } else {
        callBack()
      }
    }

    window.addEventListener('scroll', handleScroll)

    return () => window.removeEventListener('scroll', handleScroll)
  }, deps)
}


Enter fullscreen mode Exit fullscreen mode

让我们来仔细分析一下,看看它内部究竟发生了什么。

为了支持服务器端渲染 (SSR) 并防止不必要的错误,我们需要检查是否DOM已准备就绪以及window上下文是否存在。最简单的方法是检查是否已window就绪defined



const isBrowser = typeof window !== `undefined`


Enter fullscreen mode Exit fullscreen mode

现在我需要一个简单的函数来获取当前的滚动位置:



function getScrollPosition({ element, useWindow }) {
  if (!isBrowser) return { x: 0, y: 0 }

  const target = element ? element.current : document.body
  const position = target.getBoundingClientRect()

  return useWindow
    ? { x: window.scrollX, y: window.scrollY }
    : { x: position.left, y: position.top }
}


Enter fullscreen mode Exit fullscreen mode

这里我们检查它是否在内部运行browser,否则就返回{ x: 0, y: 0 }默认值。

下一部分很简单,我们检查用户请求的是整个页面的滚动位置,还是页面中某个特定元素的滚动位置。

const target = element ? element.current : document.body

元素通过引用传递给函数,该引用由useRefhook 创建,因此我们可以通过element.current值来访问它。

有很多方法可以获取当前滚动位置。
但目前最常用且最适合移动设备的方法是window.scroll使用target.getBoundingClientRect()`get_scroll_position` 和 `get_scroll_position`。它们的性能略有不同,各有用途,因此我们引入了参数切换器,让用户自行选择使用哪种实现方式useWindow

不过请注意,Window.scroll[X|Y]IE(11 或更低版本)不支持此功能。IE9 及更低版本(在大多数情况下)不应再受支持,因为使用这些版本意味着浏览器或操作系统将无法获得安全更新。但是,如果您仍想支持此浏览器,请将其替换为Window.page[X|Y]Offset

getBoundingClientRect()是一个强大的方法,可以获取元素边界框相对于视口的大小和位置。

caniuse称,它受98.66%所有现代浏览器支持,包括 IE9+。

现在,有了辅助函数,让我们来看看钩子本身。



export function useScrollPosition(effect, deps, element, useWindow, wait) {
  const position = useRef(getScrollPosition({ useWindow }))

  let throttleTimeout = null

  const callBack = () => {
    const currPos = getScrollPosition({ element, useWindow })
    effect({ prevPos: position.current, currPos })
    position.current = currPos
    throttleTimeout = null
  }

  useLayoutEffect(() => {
    const handleScroll = () => {
      if (wait) {
        if (throttleTimeout === null) {
          throttleTimeout = setTimeout(callBack, wait)
        }
      } else {
        callBack()
      }
    }

    window.addEventListener('scroll', handleScroll)

    return () => window.removeEventListener('scroll', handleScroll)
  }, deps)
}


Enter fullscreen mode Exit fullscreen mode

为了存储当前位置坐标,我们引入有状态position变量。

const position = useRef(getScrollPosition({ useWindow }))

请注意,我使用的是 `ref`useRef()而不是 `this` useState()。根据React-Hooks 参考指南useRef()`ref` 的用途远不止于 `ref` 属性。它便于保存任何可变值,类似于在类中使用实例字段的方式。

这正是我们需要的,一个有状态的值,它不会re-render在每次状态改变时都触发。

由于我们的钩子函数与某些功能紧密相关,因此DOM我们需要将其实现在另一个Effect钩子函数内部。默认情况下,特效会在每次渲染完成后运行,但您可以选择仅在特定值发生变化时触发它。

React 目前支持两种类型的Effecthooks:useEffectuseLayoutEffect

在我们的例子中,最佳选择是使用 `react.on` useLayoutEffect,它会在 React 完成所有 DOM 操作后立即同步运行。如果您需要进行 DOM 测量(例如获取元素的滚动位置或其他样式),然后通过更新状态来执行 DOM 操作或触发同步重新渲染,这将非常有用。

就调度而言,它的工作方式与componentDidMount和相同componentDidUpdate。您的代码会在DOM更新后立即运行,但在浏览器有机会“绘制”这些更改之前运行(用户实际上要等到浏览器重新绘制后才能看到更新)。

如果你查看一下钩子函数,你会注意到这个deps参数。
我们将使用它向钩子传递一个自定义依赖项数组,强制钩子在依赖项状态改变时重新渲染,从而避免任何不必要的渲染。



const [hideOnScroll, setHideOnScroll] = useState(true)

useScrollPosition(({ prevPos, currPos }) => {
  const isShow = currPos.y > prevPos.y
  if (isShow !== hideOnScroll) setHideOnScroll(isShow)
}, [hideOnScroll])


Enter fullscreen mode Exit fullscreen mode

例如,这里我们开始使用钩子跟踪滚动位置useScrollPosition,它会在每次位置变化时分别返回prevPos,并在变化currPos时重新渲染自身,我们需要这样做,因为是一个有状态的变量,它会在其变化时触发组件重新渲染,从而触发清理例程()。hideOnScrollhideOnScrolluseScrollPositioncomponentWillUnmount



useLayoutEffect(() => {
window.addEventListener('scroll', handleScroll)

return () => window.removeEventListener('scroll', handleScroll)
}, deps)


Enter fullscreen mode Exit fullscreen mode

所以,这里有一个effect程序,它会在某个时刻启动事件监听器componentDidMount,并在某个时刻移除它componentWillUnmount,并且只有当任何deps状态发生改变时才会重新启动自身。

最后,让我们看一下我们的程序handler,它会在每次滚动位置改变时运行。

请注意,跟踪当前滚动位置可能非常耗费资源,并会给您的应用程序带来不必要的负载,从而大大降低用户体验!

这意味着我们需要找到一种方法来尽可能地优化这个例程!
根据这篇 MDN文章,我们可以使用某种requestAnimationFrame方法来优化我们的函数。事实上,这种模式非常常被使用/复制,尽管它在实践中几乎没有任何意义,而且在Stack Overflow上已经有详尽的解释和讨论,所以我不会深入探讨。主要的结论是,老一套的setTimeout()方法才是限制性能的最佳方案scroll event



export function useScrollPosition(effect, deps, element, useWindow, wait) {

  let throttleTimeout = null

  const callBack = () => {
    ...
    throttleTimeout = null
  }

  useLayoutEffect(() => {
    const handleScroll = () => {
      if (wait) {
        if (throttleTimeout === null) {
          throttleTimeout = setTimeout(callBack, wait)
        }
      } else {
        callBack()
      }
    }

    ...
  }, deps)
}


Enter fullscreen mode Exit fullscreen mode

这里,wait参数是一个时间段ms,我们用它来控制函数的执行频率。这意味着,只有在等待时间结束后,函数scroll event才会更新位置值并调用后续函数。callback

我已经制作了一个可用于生产环境的模块,所以您可以立即安装并使用我们刚刚学到的知识!

GitHub 标志 n8tb1t /使用滚动位置

正确使用 ReactJS hook 的滚动定位功能

use-scroll-position

Node 版本 Node 版本 Node 版本

截屏

use-scroll-position这是一个 React Hook,用于返回浏览器视口的 X 和 Y 轴滚动位置。它经过高度优化,并使用特殊技术来避免不必要的重新渲染!

它使用默认的 React Hooks 渲染生命周期,这使您可以完全控制其行为并防止不必要的渲染。

重要更新通知

从 v1.0.44 版本开始,该项目已迁移到 TypeScript。

此外,一些错误也得到了修复,感谢我们的贡献者,我们添加了一个选项来跟踪某些自定义容器内指定元素的滚动位置

只需传递element引用和boundingElement父容器引用,并跟踪它们对应的位置即可!(boundingElement应该可以滚动,溢出时应隐藏或其他方式)

演示

编辑使用滚动位置

安装

yarn add @n8tb1t/use-scroll-position

用法

useScrollPosition(effect,deps, element, useWindow, wait)
Enter fullscreen mode Exit fullscreen mode
论点 描述
effect 影响
文章来源:https://dev.to/n8tb1t/tracking-scroll-position-with-react-hooks-3bbj