Skip to content

useScrollAnchor ​

会话容器的滚动锚定 composable:维护「是否贴底 / 未读计数 / 回到底部」三件事,让长对话在流式输出时自动跟随、用户上翻阅读时不被打扰。

适用场景 ​

场景说明
流式输出自动跟随贴底时内容高度增长(新 token / 新消息)自动滚到最新,实现「打字机跟随」
上翻阅读不打扰用户拖离底部后停止自动滚动,期间新增内容累计未读角标
回到底部按钮isAtBottom 驱动按钮显隐,unreadCount 驱动角标,点击调用 scrollToBottom() 并清零
自定义会话容器不用 Conversation 时,任意 overflow-y: auto 容器接上 bindContainer 即可获得同款锚定行为

典型消费组件(provide / inject 关系):

  • Conversation —— 内部 useScrollAnchor() 并 provide('scrollAnchor', anchor),是上下文的唯一提供方
  • ConversationScrollBtn —— inject('scrollAnchor') 消费:按钮显隐、未读角标、点击回底全部来自本 composable 的状态

代码演示 ​

拖动容器滚离底部后追加消息,观察 isAtBottom 翻转与宿主侧未读计数:

消息 1

消息 2

消息 3

消息 4

消息 5

消息 6

消息 7

消息 8

消息 9

消息 10

isAtBottom: true · 未读(宿主维护): 0

函数签名 ​

typescript
function useScrollAnchor(threshold?: number): ScrollAnchorContext

interface ScrollAnchorContext {
  isAtBottom: Ref<boolean>
  /** 离底期间容器内容新增导致的未读计数(贴底时恒为 0) */
  unreadCount: Ref<number>
  scrollToBottom: () => void
  bindContainer: (el: HTMLElement | null) => void
}

参数 ​

参数类型默认值说明
thresholdnumber50距底部小于该像素值即视为「贴底」

行为 ​

  • 贴底判定基于 scrollHeight - scrollTop - clientHeight < threshold,滚动监听为 passive。
  • 内容高度增长走双通道检测:ResizeObserver(容器自身尺寸变化,如窗口缩放)+ MutationObserver(子树节点/文本变化)。后者是必需兜底——浏览器对 overflow 定高滚动容器的内容增高不派发容器 ResizeObserver,仅靠 RO 时计数在生产环境永不增长。贴底时自动 scrollToBottom,离底且增长时 unreadCount++(无法区分「新增消息」与「文本替换」,语义局限)。
  • 高频变化经 requestAnimationFrame 合并,每帧只做一次高度对比。
  • bindContainer 会解绑旧容器(含两类 observer)并重设未读基线,容器卸载时传 null 清理。

未读计数的语义局限

双通道检测覆盖了定高容器内容增高的场景,但「新消息」仍以内容高度增加近似——流式回复期间离底,计数会随文本追加持续上涨,也无法区分消息新增与替换。组件测试若 mock 掉了 ResizeObserver / MutationObserver 则无法覆盖该行为。精确按消息条数计数时,请像上面演示那样由宿主自行维护计数,经 ConversationScrollBtn 的 badge prop 传入。

相关 ​