Skip to page content

useScroll 组合式 API
v2.34+

useScroll() composable 通过响应式状态追踪页面(或某个可滚动容器)的滚动情况:包括 scrollPosition(滚动位置)、最后一次滚动的 scrollDirection(滚动方向)、自上次上报以来的 scrollDelta(滚动增量),以及方向最后一次改变的 scrollInflectionPoint(拐点)。

它是 QScrollObserver 组件(该组件正是构建于它之上)和 v-scroll 指令在 setup 代码中的对应物。当你想在自己的组件上、或在任意可滚动容器上获取滚动细节,而又不想为此在模板中额外添加一个节点时,就用这个 composable。

TIP

在 SSR 或 SSG 模式的服务端,该 composable 不会监听任何东西:在客户端接管之前,状态会保持它的初始值。

在组件之外使用

该 composable 也可以在 setup() 之外调用:在 boot 文件、store 或普通模块中。此时没有组件根元素可以作为检测起点,也没有挂载时机可等待,因此请提供一个 scrollTarget(或一个 target 元素);追踪会立即开始,并且不会自行停止:完成后请调用 stopScroll()。

语法

import { useTemplateRef } from 'vue'
import { useScroll } from 'quasar'

setup () {
  const scrollTarget = useTemplateRef('scrollTarget') // an Element or a component

  const {
    scrollPosition, scrollDirection, scrollDirectionChanged,
    scrollDelta, scrollInflectionPoint, refreshScroll, stopScroll
  } = useScroll({
    // all optional:
    scrollTarget,        // the scroll container; omit it for auto detection
    axis: 'vertical',    // 'vertical', 'horizontal' or 'both'
    debounce: 100,       // ms per report; omit for one per frame, 0 for every event
    disabled: false,     // pause listening
    onScroll (details) { // called with the scroll details on every change
      // ...
    }
  })

  // ...
}
function useScroll(
  options?: MaybeRefOrGetter<{
    target?: MaybeRefOrGetter<
      Element | ComponentPublicInstance | null | undefined
    >
    scrollTarget?: MaybeRefOrGetter<
      Element | Window | string | ComponentPublicInstance | null | undefined
    >
    axis?: 'vertical' | 'horizontal' | 'both'
    debounce?: string | number
    disabled?: boolean
    onScroll?: (details: {
      position: { top: number; left: number }
      direction: 'up' | 'down' | 'left' | 'right'
      directionChanged: boolean
      delta: { top: number; left: number }
      inflectionPoint: { top: number; left: number }
    }) => void
  }>
): {
  scrollPosition: ShallowRef<{ top: number; left: number }>
  scrollDirection: Ref<'up' | 'down' | 'left' | 'right'>
  scrollDirectionChanged: Ref<boolean>
  scrollDelta: ShallowRef<{ top: number; left: number }>
  scrollInflectionPoint: ShallowRef<{ top: number; left: number }>
  refreshScroll: () => void
  stopScroll: () => void
}

这些响应式状态镜像了 QScrollObserver 所发出(也是 onScroll 所接收)的细节:scrollPosition、scrollDelta 和 scrollInflectionPoint 都是带有 top 和 left 偏移量(以像素为单位)的对象,scrollDirection 是最后一次滚动的方向,而 scrollDirectionChanged 则表明这最后一次滚动是否反转了方向。

追踪的是哪个容器

在不传选项的情况下,该 composable 遵循 Quasar 所有滚动组件和指令使用的同一套算法:从它所在组件的根元素出发(以组件挂载的那一刻为准),向上寻找最近的、带有 scroll、scroll-y 或 overflow-auto CSS 类的父元素;如果一个都找不到,就直接监听页面本身。

  • scrollTarget 直接指定容器:可以是一个 Element(或 window)、一个 CSS 选择器,或一个组件实例(代表它的根元素),与滚动组件的 scroll-target 属性含义相同。
  • target 改变自动检测的起点:一个元素或组件的 ref,其最近的可滚动父元素就是你想要的容器。在渲染 fragment(多个根节点)、没有根元素可作起点的组件中,你需要用它。

只要容器可用,第一次上报就会立即发生(即便它当时已经处于滚动状态);onScroll 同样会为此被调用一次,之后仅在所监视的 axis 上位置发生变化时才会再次调用。

不设置 debounce 时,无论浏览器在两帧之间触发了多少次滚动事件,该 composable 每个动画帧最多上报一次。设置 debounce: 0 时,它会在每一次滚动事件上都上报。设置为若干毫秒时,它在每个这么长的时间窗口内最多上报一次;但最后一次变化绝不会被漏掉。

refreshScroll() 会立即读取当前位置,跳过 debounce。你很少会需要它,因为浏览器会自行上报每一次滚动。

stopScroll() 会彻底结束追踪。你同样很少会需要它,因为组件销毁时该 composable 会自行停止。

运行时更改选项

选项可以是一个普通对象、一个 Ref 或一个 getter 函数。普通对象只读取一次。使用 Ref 或 getter 时,该 composable 会追踪选项所读取的任何响应式状态,并在该状态变化时重新应用它们,因此你永远不需要调用任何东西来「更新」它:

  • 切换 disabled 会暂停和恢复追踪(暂停期间状态保持其最后的值,恢复时如果位置在此期间发生了移动会立即上报)
  • 把 scrollTarget(或 target)指向另一个容器会随之跟踪,并从头开始
  • 更改 axis 或 debounce 从下一次滚动起生效
  • 替换 onScroll 从下一次滚动起生效
import { ref } from 'vue'
import { useScroll } from 'quasar'

setup () {
  const paused = ref(false)

  const { scrollPosition } = useScroll(() => ({
    disabled: paused.value
  }))

  function pause () { paused.value = true }
  function resume () { paused.value = false }

  // ...
}

示例

下面这个示例通过一个 template ref 追踪某个可滚动容器,并展示 composable 上报的每一个细节:

<template>
  <div class="q-pa-md">
    <div ref="boxRef" class="scroll box rounded-borders q-mb-md">
      <div v-for="n in 30" :key="n" class="q-pa-sm">Line #{{ n }}</div>
    </div>

    <div class="q-gutter-sm">
      <q-badge :label="`top: ${scrollPosition.top}`" />
      <q-badge :label="`direction: ${scrollDirection}`" />
      <q-badge :label="`delta: ${scrollDelta.top}`" />
      <q-badge :label="`inflection point: ${scrollInflectionPoint.top}`" />
      <q-badge
        :color="scrollDirectionChanged ? 'positive' : 'grey'"
        :label="scrollDirectionChanged ? 'direction changed' : 'same direction'"
      />
    </div>
  </div>
</template>

<script setup>
import { useTemplateRef } from 'vue'
import { useScroll } from 'quasar'

const boxRef = useTemplateRef('boxRef')

const {
  scrollPosition,
  scrollDirection,
  scrollDirectionChanged,
  scrollDelta,
  scrollInflectionPoint
} = useScroll({ scrollTarget: boxRef })
</script>

<style lang="sass" scoped>
.box
  height: 200px
  border: 1px solid #fff
  outline: 1px solid #000
</style>

下面这个示例演示的是页面级滚动:在不传选项的情况下,自动检测会从组件的根元素开始,并在标准布局下最终落到页面本身。把本页向下滚动到示例下方、再往回上滚一点:一旦页面被向下滚动过、而用户又开始向上滚动,「回到顶部」按钮就会被启用:

<template>
  <div class="q-pa-md">
    <div class="q-gutter-sm q-mb-md">
      <q-badge :label="`page top: ${scrollPosition.top}`" />
      <q-badge :label="`direction: ${scrollDirection}`" />
    </div>

    <q-btn
      color="primary"
      push
      icon="keyboard_arrow_up"
      label="Back to top"
      :disable="scrollPosition.top <= 300 || scrollDirection !== 'up'"
      @click="scrollToTop"
    />
  </div>
</template>

<script setup>
import { scroll, useScroll } from 'quasar'

// no options: the auto detection starts from this component's root
// element and ends up on the page itself
const { scrollPosition, scrollDirection } = useScroll()

function scrollToTop() {
  scroll.setVerticalScrollPosition(window, 0, 300)
}
</script>