Skip to content

滚动感知 useScrollParent

赞助

查询并监听最近可滚动父元素,响应视口 resize 的组合式函数

用于在可滚动容器内正确跟随滚动并维护定位。

Show Source Code
ts
import { ref, computed, watch, onBeforeUnmount, onMounted } from 'vue'
import type { Ref } from 'vue'
import { useOptionsSupported, useEventListener, getScrollParent } from 'vue-amazing-ui'
// 注:ScrollTarget / resolveScrollEventTarget 为组件库内部共用的滚动目标解析工具(详见 useScroll 源码)
export interface ScrollParentOptions {
  passive?: boolean // 是否使用 passive 滚动监听,默认跟随浏览器支持情况
  onCleanup?: () => void // 附加清理:组件自身需在 cleanup 时执行的逻辑(如 Tooltip 取消位置更新帧)
}
export function useScrollParent(
  contentRef: Ref<HTMLElement | null>,
  onScroll: () => void,
  options: ScrollParentOptions = {}
): {
  scrollTarget: Ref<HTMLElement | null>
  viewportWidth: Ref<number>
  viewportHeight: Ref<number>
  observeScroll: () => void
  cleanup: () => void
} {
  const scrollTarget = ref<HTMLElement | null>(null) // 最近的可滚动父元素
  let scrollEventTarget: ScrollTarget | null = null // 实际承载 scroll 事件的监听目标
  // SSR(Node)环境无 document,取 0;浏览器端初始值与原来一致
  const viewportWidth = ref(typeof document !== 'undefined' ? document.documentElement.clientWidth : 0)
  const viewportHeight = ref(typeof document !== 'undefined' ? document.documentElement.clientHeight : 0)
  const { isSupported: passiveSupported } = useOptionsSupported('passive')
  const usePassive = options.passive !== false && passiveSupported.value

  // 更新视口尺寸,重新查询滚动父元素并触发重排
  function getViewportSize() {
    viewportWidth.value = document.documentElement.clientWidth
    viewportHeight.value = document.documentElement.clientHeight
    observeScroll()
    onScroll()
  }

  // 查询并监听最近可滚动父元素:整页滚动(scrollTarget 为 documentElement)时
  // scroll 事件派发在 window 上,documentElement 收不到,故监听目标需经 resolveScrollEventTarget 解析
  function observeScroll() {
    cleanup()
    scrollTarget.value = getScrollParent(contentRef.value)
    scrollEventTarget = resolveScrollEventTarget(scrollTarget.value)
    scrollEventTarget?.addEventListener('scroll', onScroll, usePassive ? { passive: true } : undefined)
  }

  // 清理滚动监听并重置滚动目标(含组件注入的附加清理)
  function cleanup() {
    scrollEventTarget?.removeEventListener('scroll', onScroll)
    scrollEventTarget = null
    scrollTarget.value = null
    options.onCleanup?.()
  }

  // 实参 window 在 setup 期求值,SSR(Node)下必须先判断存在性再调用
  if (typeof window !== 'undefined') {
    useEventListener(window, 'resize', getViewportSize)
  }
  onMounted(observeScroll)
  onBeforeUnmount(cleanup)

  return { scrollTarget, viewportWidth, viewportHeight, observeScroll, cleanup }
}

基本使用

实现弹出面板在可滚动容器内跟随滚动


滚动我

最近可滚动父元素:未找到

滚动触发次数:0

视口宽度:0

视口高度:0

vue
<script setup lang="ts">
import { computed, ref } from 'vue'
import { useScrollParent } from 'vue-amazing-ui'
const contentRef = ref<HTMLElement | null>(null)
const scrollCount = ref(0)
const { scrollTarget, viewportWidth, viewportHeight } = useScrollParent(contentRef, () => {
  scrollCount.value++
})
const scrollTargetName = computed(() => {
  const target = scrollTarget.value
  if (!target) return '未找到'
  const className = target.className ? `.${target.className}` : ''
  return `${target.tagName.toLowerCase()}${className}`
})
</script>
<template>
  <div class="scroll-container">
    <div ref="contentRef" class="scroll-content">滚动我</div>
  </div>
  <Card :body-style="{ fontSize: '16px' }">
    <p>最近可滚动父元素:{{ scrollTargetName }}</p>
    <p>滚动触发次数:{{ scrollCount }}</p>
    <p>视口宽度:{{ viewportWidth }}</p>
    <p>视口高度:{{ viewportHeight }}</p>
  </Card>
</template>
<style lang="less" scoped>
.scroll-container {
  width: 500px;
  height: 360px;
  border: 2px solid #1677ff;
  border-radius: 12px;
  overflow: auto;
  .scroll-content {
    width: 800px;
    height: 600px;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 20px;
    font-weight: 500;
  }
}
</style>

Params

参数说明类型默认值
contentRef触发器内容元素,用于向上查找可滚动父元素Ref<HTMLElement | null>undefined
onScroll滚动/resize 触发的回调函数() => voidundefined
options配置项ScrollParentOptions{}

ScrollParentOptions

参数说明类型默认值
passive是否使用 passive 滚动监听boolean跟随浏览器支持情况
onCleanup附加清理,组件自身需在 cleanup 时执行的逻辑() => voidundefined

Return

名称说明类型
scrollTarget最近的可滚动父元素Ref<HTMLElement | null>
viewportWidth视口宽度Ref<number>
viewportHeight视口高度Ref<number>
observeScroll查询并监听最近可滚动父元素() => void
cleanup清理滚动监听并重置滚动目标() => void

注意事项

  • SelectAutoCompleteTooltip 等弹出类组件内部使用,也可脱离组件库独立复用
  • 无滚动祖先(整页滚动)时内部会把 scroll 监听绑定到 window:视口滚动的事件目标是 window / documentdocumentElement 收不到 scroll
  • 需在组件 setup 中调用;内部会在挂载时自动监听、卸载时自动清理,也可手动调用 observeScroll / cleanup
  • viewportWidth / viewportHeight 基于 documentElementclientWidth / clientHeight,仅随视口 resize 更新

Released under the MIT License.