Skip to content
useScrollAnchor
KnowlegeBase 知识库

AI Chatbot 中的 useScrollAnchor Hook

介绍 Vercel AI Chatbot 中 useScrollAnchor Hook.

代码地址:https://github.com/vercel/ai-chatbot/blob/main/lib/hooks/use-scroll-anchor.tsx

使用情况

tsx
...
const { messagesRef, scrollRef, visibilityRef, isAtBottom, scrollToBottom } =
  useScrollAnchor()

return (
  <div
    className="group w-full overflow-auto pl-0 peer-[[data-state=open]]:lg:pl-[250px] peer-[[data-state=open]]:xl:pl-[300px]"
    ref={scrollRef}
  >
    <div
      className={cn('pb-[200px] pt-4 md:pt-10', className)}
      ref={messagesRef}
    >
      {messages.length ? (
        <ChatList messages={messages} isShared={false} session={session} />
      ) : (
        <EmptyScreen />
      )}
      <div className="h-px w-full" ref={visibilityRef} />
    </div>
    <ChatPanel
      id={id}
      input={input}
      setInput={setInput}
      isAtBottom={isAtBottom}
      scrollToBottom={scrollToBottom}
    />
  </div>
)
}

useScrollAnchor 的定义

ts
import { useCallback, useEffect, useRef, useState } from 'react'

export const useScrollAnchor = () => {
  const messagesRef = useRef<HTMLDivElement>(null)
  const scrollRef = useRef<HTMLDivElement>(null)
  const visibilityRef = useRef<HTMLDivElement>(null)

  const [isAtBottom, setIsAtBottom] = useState(true)
  const [isVisible, setIsVisible] = useState(false)

  const scrollToBottom = useCallback(() => {
    if (messagesRef.current) {
      messagesRef.current.scrollIntoView({
        block: 'end',
        behavior: 'smooth'
      })
    }
  }, [])

  useEffect(() => {
    if (messagesRef.current) {
      if (isAtBottom && !isVisible) {
        messagesRef.current.scrollIntoView({
          block: 'end'
        })
      }
    }
  }, [isAtBottom, isVisible])

  useEffect(() => {
    const { current } = scrollRef

    if (current) {
      const handleScroll = (event: Event) => {
        const target = event.target as HTMLDivElement
        const offset = 25
        const isAtBottom =
          target.scrollTop + target.clientHeight >= target.scrollHeight - offset

        setIsAtBottom(isAtBottom)
      }

      current.addEventListener('scroll', handleScroll, {
        passive: true
      })

      return () => {
        current.removeEventListener('scroll', handleScroll)
      }
    }
  }, [])

  useEffect(() => {
    if (visibilityRef.current) {
      let observer = new IntersectionObserver(
        entries => {
          entries.forEach(entry => {
            if (entry.isIntersecting) {
              setIsVisible(true)
            } else {
              setIsVisible(false)
            }
          })
        },
        {
          rootMargin: '0px 0px -150px 0px'
        }
      )

      observer.observe(visibilityRef.current)

      return () => {
        observer.disconnect()
      }
    }
  })

  return {
    messagesRef,
    scrollRef,
    visibilityRef,
    scrollToBottom,
    isAtBottom,
    isVisible
  }
}

这段代码定义了一个自定义的 React Hook,名为 useScrollAnchor。这个 Hook 主要用于处理与滚动相关的行为。以下是各个部分的详细解释:

  • messagesRefscrollRefvisibilityRef 是 React refs,分别引用三个不同的 HTMLDivElement。这些 refs 可能用于直接操作 DOM 元素,例如滚动到特定位置。
  • isAtBottomisVisible 是两个状态变量。isAtBottom 表示是否已经滚动到底部,isVisible 表示是否可见。
  • scrollToBottom 是一个回调函数,用于滚动到 messagesRef 所引用的元素的底部。这个函数会在 messagesRef 当前引用了一个元素时,将这个元素滚动到视图的底部。

三个 useEffect

  • 第一个 useEffect 钩子函数在 isAtBottomisVisible 变化时触发。如果当前已经滚动到底部且不可见,那么会将 messagesRef 所引用的元素滚动到视图的底部。
  • 第二个 useEffect 钩子函数在组件挂载时添加一个滚动事件监听器到 scrollRef 所引用的元素。这个监听器会在滚动时检查是否已经滚动到底部,并更新 isAtBottom 的状态。
  • 第三个 useEffect 钩子函数在组件挂载时创建一个 Intersection Observer,用于检测 visibilityRef 所引用的元素是否在视口中。如果在视口中,那么 isVisible 会被设置为 true,否则会被设置为 false

这个 Hook 可能用于一个聊天应用或者任何需要自动滚动到底部的场景,例如日志显示。

isVisible

ts
useEffect(() => {
  if (visibilityRef.current) {
    let observer = new IntersectionObserver(
      entries => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            setIsVisible(true)
          } else {
            setIsVisible(false)
          }
        })
      },
      {
        rootMargin: '0px 0px -150px 0px'
      }
    )

    observer.observe(visibilityRef.current)

    return () => {
      observer.disconnect()
    }
  }
})

这段代码是一个useEffect钩子,它使用了IntersectionObserver API来检测一个元素是否在浏览器的视口(viewport)内。这个观察器对于实现诸如懒加载、无限滚动、视口触发的动画等特性非常有用。下面是对这个useEffect钩子的详细解释:

  1. useEffect 钩子的作用

    • useEffect是React中用于处理副作用(side effects)的钩子,比如数据获取、订阅、定时器等。在这个例子中,它用于设置一个观察器,该观察器会在组件渲染后开始工作。
  2. 检查visibilityRef.current

    • visibilityRef是一个通过useRef创建的引用对象,它被用来引用页面上的一个特定元素。在这个useEffect钩子中,首先检查visibilityRef.current是否存在,确保它已经被正确地关联到一个DOM元素。
  3. 创建IntersectionObserver实例

    • IntersectionObserver是一个用于异步观察目标元素与其祖先元素或顶级文档视口(viewport)交叉状态的API。在这个例子中,创建了一个观察器实例observer,并传入了两个参数:
      • 第一个参数是一个回调函数,它在观察到的元素与视口交叉状态发生变化时被调用。
      • 第二个参数是观察器的配置对象,其中rootMargin属性定义了视口的边界外多少像素应该被计算在内。在这个例子中,rootMargin被设置为'0px 0px -150px 0px',这意味着观察器会考虑视口下方150像素之外的元素。
  4. 回调函数的处理逻辑

    • IntersectionObserver的回调函数被触发时,它会遍历所有发生变化的条目(entries)。对于每个条目,如果entry.isIntersectingtrue,则表示元素至少部分在视口内;如果为false,则表示元素完全不在视口内。根据这个状态,setIsVisible函数被调用,更新isVisible状态,这个状态可以用来控制某些UI元素的显示或隐藏。
  5. 开始观察

    • 使用observer.observe(visibilityRef.current)方法,开始观察visibilityRef.current关联的元素。当该元素的交叉状态发生变化时,IntersectionObserver的回调函数会被执行。
  6. 清理函数

    • useEffect钩子返回一个清理函数,这个函数在组件卸载或visibilityRef更新时被调用。在清理函数中,使用observer.disconnect()方法来停止观察器的工作,这是一个好的实践,可以防止内存泄漏。

总结来说,这段代码通过IntersectionObserver API来检测一个元素是否在视口内,并且根据这个状态更新组件的状态isVisible,这个状态可以用于控制其他UI元素的显示或隐藏。

第二个参数是观察器的配置对象,其中rootMargin属性定义了视口的边界外多少像素应该被计算在内。在这个例子中,rootMargin被设置为'0px 0px -150px 0px',这意味着观察器会考虑视口下方150像素之外的元素。 这个意味着什么?

在这个例子中,rootMargin属性设置为'0px 0px -150px 0px',这个配置对IntersectionObserver的行为有特定的影响。rootMargin的值是一个四值语法,分别对应左、上、右、下的边距。这些值是相对于根元素(在这个例子中是视口)的边界来定义的。

具体来说,'0px 0px -150px 0px'的设置意味着:

  • 左和右的边距为0px:元素与视口左右边界的交叉检测不会考虑额外的边距。
  • 上边距为0px:元素与视口上边界的交叉检测也不会考虑额外的边距。
  • 下边距为-150px:这是一个负值,意味着观察器会将视口下边界向下扩展150像素。也就是说,即使元素的底部实际上在视口下方150像素之外,它仍然可以被认为是与视口交叉的。

这个设置的实际效果是,当元素的顶部进入视口,并且至少有150像素的高度在视口之内时,entry.isIntersecting就会返回true。这允许开发者更早地知道元素即将进入视口,从而可以触发一些操作,比如预加载内容或者准备动画效果。

例如,在聊天应用中,如果消息列表接近底部,你可能希望提前加载更多消息。通过设置rootMargin'0px 0px -150px 0px',你可以确保在用户接近列表底部时,即当列表的底部距离视口还有150像素的距离时,就触发加载更多消息的操作。这样可以提供更流畅的用户体验,因为他们在滚动时不会遇到突然的内容中断。

可以这么说,当visibilityRef在浏览器视窗下150px 时,就被认为相交了?视为可见?

是的,您的理解是正确的。当visibilityRef关联的元素的顶部距离浏览器视窗的底部少于150像素时,IntersectionObserver就会认为该元素与视口相交,即视为可见。这是因为rootMargin的设置'0px 0px -150px 0px'使得观察器在视口的底部向下扩展了150像素。

这种情况下,即使整个元素还没有完全进入视窗范围内,只要它的顶部进入了视窗的这150像素的区域,entry.isIntersecting就会返回true,触发IntersectionObserver的回调函数。这样,您可以知道元素即将变得可见,并据此执行相应的操作,比如加载数据、触发动画或者更新状态等。

isBottom

这段代码是一个React的副作用钩子(useEffect),它用于处理滚动事件。

  1. useEffect 是一个React Hook,它接受一个函数作为参数,这个函数会在组件渲染后执行。这里的 [] 表示这个副作用只在组件挂载和卸载时运行,而不是在每次渲染后运行。

  2. 在这个函数中,首先获取 scrollRefcurrent 属性。scrollRef 是一个React ref,它的 current 属性指向了一个DOM元素。

  3. 如果 current 存在,那么定义一个处理滚动事件的函数 handleScroll。这个函数接受一个事件对象作为参数。

  4. handleScroll 函数中,首先获取事件的目标元素,并将其类型断言为 HTMLDivElement

  5. 定义一个偏移量 offset,然后计算目标元素是否滚动到了底部。如果元素的滚动高度加上元素的客户端高度大于等于元素的滚动高度减去偏移量,那么就认为元素滚动到了底部。

  6. 然后调用 setIsAtBottom 函数,将 isAtBottom 的值设置为计算得到的结果。

  7. 使用 addEventListener 方法为 current 添加一个滚动事件监听器,当滚动事件发生时,调用 handleScroll 函数。这里的 { passive: true } 是一个选项对象,表示这个事件监听器是被动的,即它不会阻止事件的默认行为。

  8. 最后,返回一个清理函数,这个函数会在组件卸载时运行,用于移除添加的事件监听器。

这段代码是一个useEffect钩子,它的作用是在组件挂载后设置一个滚动事件监听器,用于检测一个滚动容器是否滚动到了接近底部的状态。下面是对这个useEffect钩子的详细解释:

  1. 获取scrollRef的当前值

    • scrollRef是一个通过useRef创建的引用对象,它被用来引用页面上的一个滚动容器(如一个<div>元素)。
    • const { current } = scrollRef这行代码从scrollRef对象中解构出current属性,这个属性是引用的DOM元素。
  2. 检查引用是否关联到了实际的DOM元素

    • if (current)这个条件判断确保了scrollRef已经被关联到一个实际的DOM元素,而不是null
  3. 定义滚动事件处理函数handleScroll

    • 当滚动事件发生时,handleScroll函数会被调用。
    • event.target是触发事件的元素,这里通过类型断言将其转换为HTMLDivElement
    • target.scrollTop是当前滚动容器的垂直滚动位置。
    • target.clientHeight是滚动容器的可见高度。
    • target.scrollHeight是滚动容器的总高度,包括超出当前视图的部分。
  4. 计算是否到达底部

    • isAtBottom是一个布尔值,它通过比较target.scrollTop加上target.clientHeight是否大于或等于target.scrollHeight减去一个偏移量offset来确定。
    • 如果isAtBottomtrue,则表示用户已经滚动到了滚动容器的底部,或者非常接近底部,因为offset提供了一定的容差。
  5. 设置和清理滚动事件监听器

    • 使用current.addEventListener('scroll', handleScroll, { passive: true }),将handleScroll函数作为滚动事件的监听器添加到当前的滚动容器上。
    • { passive: true }是一个选项对象,它告诉浏览器这个事件监听器不会调用preventDefault方法来阻止默认行为,这通常用于优化滚动性能。
    • return () => { current.removeEventListener('scroll', handleScroll) }useEffect钩子的返回值,它是一个清理函数。
    • 这个清理函数在组件卸载时被调用,用于移除之前添加的滚动事件监听器,以避免内存泄漏和潜在的副作用。

总结来说,这个useEffect钩子的作用是在组件挂载后开始监听一个滚动容器的滚动事件,当用户滚动到容器的接近底部时,更新isAtBottom状态。这个状态可以用来触发加载更多数据、显示加载指示器或其他相关操作。

在JavaScript中,passiveaddEventListener 方法的一个选项。它是一个布尔值,用于改变浏览器处理事件的方式。

passive 设置为 true 时,表示该事件的处理函数不会调用 preventDefault 方法阻止事件的默认行为。这可以提高页面的滚动性能,特别是在移动设备上。

这是因为在默认情况下,浏览器会等待事件处理函数执行完毕,以确定是否调用了 preventDefault 方法。如果调用了,浏览器就不会执行事件的默认行为。这可能会导致页面的滚动延迟,因为浏览器需要等待事件处理函数执行完毕。

但是,如果设置了 passive: true,浏览器就知道事件处理函数不会阻止事件的默认行为,所以它可以立即开始执行默认行为,不需要等待事件处理函数执行完毕。这可以减少滚动延迟,提高页面的滚动性能。

offset

在这段代码中,offset起到了一个缓冲区的作用,它允许开发者定义一个容差范围,以确定何时认为用户已经滚动到了滚动容器的“底部”。这个概念在处理滚动事件时非常有用,因为它可以帮助避免在用户实际还未到达内容底部时就触发底部到达的逻辑。

具体来说,offset的值设置为25,这意味着:

  • 当滚动容器的滚动位置(scrollTop)加上容器的可见高度(clientHeight)接近容器的总高度(scrollHeight)时,减去offset值后,如果这个和大于或等于容器总高度与offset的差值,isAtBottom就会被设置为true
  • 换句话说,即使用户还没有完全滚动到容器的最底部,只要距离底部的距离小于或等于offset值,isAtBottom也会被认为是true

这个offset的作用是:

  1. 防止过早触发:如果用户还没有滚动到足够接近底部的位置,不应该触发底部到达的逻辑,offset可以防止这种情况发生。
  2. 平滑用户体验:用户在滚动时,可能会期望在接近底部时就看到某些动作(例如加载更多内容),而不是必须精确滚动到底部。offset提供了这种平滑的过渡。
  3. 容错处理:由于滚动事件可能会因为各种原因(如浏览器渲染延迟)而不够精确,offset提供了一定的容错空间,确保逻辑的健壮性。

总的来说,offset在这里作为一个缓冲值,确保了在用户接近滚动容器底部时,相关的操作(如加载更多数据)能够及时触发,同时避免了因为微小的滚动差异而导致的误操作。

ts
const isAtBottom =
  target.scrollTop + target.clientHeight >= target.scrollHeight - offset

这是一个JavaScript表达式,用于判断一个元素是否滚动到了底部。这里的参数是HTML元素的属性:

  • target.scrollTop: 这是元素的滚动条顶部到视口顶部的距离。换句话说,它是你已经滚动过的像素数。

  • target.clientHeight: 这是元素的内部高度(以像素为单位),包括内边距,但不包括水平滚动条、边框和外边距。

  • target.scrollHeight: 这是元素的整体高度(以像素为单位),包括元素的内边距,但不包括边框、外边距或水平滚动条。如果元素没有垂直滚动条,那么这个值等于元素的视口高度。

  • offset: 这是一个自定义的偏移量,用于微调滚动到底部的判断。例如,如果你希望在元素滚动到接近底部时就认为它已经滚动到底部,那么你可以设置一个正的偏移量。

这个表达式的意思是,如果元素已经滚动的距离加上元素的视口高度大于等于元素的整体高度减去偏移量,那么就认为元素已经滚动到了底部。

isBottom 的使用

在 ChatPanel,有一个 ButtonScrollToBottom

tsx
<ButtonScrollToBottom
  isAtBottom={isAtBottom}
  scrollToBottom={scrollToBottom}
/>

ButtonScrollToBottom

tsx
'use client'

import * as React from 'react'

import { cn } from '@/lib/utils'
import { Button, type ButtonProps } from '@/components/ui/button'
import { IconArrowDown } from '@/components/ui/icons'

interface ButtonScrollToBottomProps extends ButtonProps {
  isAtBottom: boolean
  scrollToBottom: () => void
}

export function ButtonScrollToBottom({
  className,
  isAtBottom,
  scrollToBottom,
  ...props
}: ButtonScrollToBottomProps) {
  return (
    <Button
      variant="outline"
      size="icon"
      className={cn(
        'absolute right-4 top-1 z-10 bg-background transition-opacity duration-300 sm:right-8 md:top-2',
        isAtBottom ? 'opacity-0' : 'opacity-100',
        className
      )}
      onClick={() => scrollToBottom()}
      {...props}
    >
      <IconArrowDown />
      <span className="sr-only">Scroll to bottom</span>
    </Button>
  )
}

这是一个名为ButtonScrollToBottom的React组件。它扩展了ButtonProps,添加了两个新的属性:isAtBottomscrollToBottom

  • isAtBottom是一个布尔值,表示是否已经滚动到底部。
  • scrollToBottom是一个函数,当调用时,会滚动到底部。

这个组件返回一个Button组件,其样式和行为由以下属性决定:

  • variantsize属性定义了按钮的外观。
  • className属性定义了按钮的CSS类,这些类控制了按钮的位置、背景、过渡效果等。如果isAtBottom为真,按钮的透明度将为0(即不可见),否则为100(完全可见)。
  • onClick属性定义了当按钮被点击时的行为,即调用scrollToBottom函数,使页面滚动到底部。

(本页部分内容由 AI 辅助生成,仅供参考,使用前请仔细鉴别。)

最新更新:

Alang.AI - Make Great AI Applications