4.7 Sensors – useScroll
https://vueuse.org/core/useScroll/
作用
响应式的监听滚动位置和状态。
官方示例
<script setup lang="ts">javascript">
import { useScroll } from '@vueuse/core'const el = ref<HTMLElement | null>(null)
const { x, y, isScrolling, arrivedState, directions } = useScroll(el)
</script><template><div ref="el" />
</template>
- 带偏移量的版本
const { x, y, isScrolling, arrivedState, directions } = useScroll(el, {offset: { top: 30, bottom: 30, right: 30, left: 30 },
})
- 手动设置滚动位置
<script setup lang="ts">javascript">
import { useScroll } from '@vueuse/core'const el = ref<HTMLElement | null>(null)
const { x, y } = useScroll(el)
</script><template><div ref="el" /><!-- 手动修改滚动偏移量 --><button @click="x += 10">Scroll right 10px</button><button @click="y += 10">Scroll down 10px</button>
</template>
-
平滑滚动
设置
behavior: smooth
实现平滑滚动.。默认行为是auto
,也就是非平滑滚动。更多滚动行为,请看https://developer.mozilla.org/en-US/docs/Web/API/Window/scrollTo
import { useScroll } from '@vueuse/core'const el = ref<HTMLElement | null>(null)
const { x, y } = useScroll(el, { behavior: 'smooth' })// Or as a `ref`:
const smooth = ref(false)
const behavior = computed(() => smooth.value ? 'smooth' : 'auto')
// 通过options传递
const { x, y } = useScroll(el, { behavior })
- 无渲染组件代码,同样支持传递回调函数和数组
<script setup lang="ts">javascript">
import type { UseScrollReturn } from '@vueuse/core'
import { vScroll } from '@vueuse/components'const data = ref([1, 2, 3, 4, 5, 6])function onScroll(state: UseScrollReturn) {console.log(state) // {x, y, isScrolling, arrivedState, directions}
}
</script><template><div v-scroll="onScroll"><div v-for="item in data" :key="item">{{ item }}</div></div><!-- with options --><div v-scroll="[onScroll, { throttle: 10 }]"><div v-for="item in data" :key="item">{{ item }}</div></div>
</template>
源码分析
- 这个
hook
的参数比较常用,先看一下
export interface UseScrollOptions {/*** 对滚动事件进行节流,默认不节流。也就是throttle时间段内,只会触发一次* @default 0毫秒。*/throttle?: number/*** 滚动结束后多久进行检查* 如果设置了throttle,这个值等于如果设置了throttle+idle* @default 200*/idle?: number/*** left 表示距离左边距多远,arrived状态会变成true。其他方向类推。*/offset?: {left?: numberright?: numbertop?: numberbottom?: number}/*** 滚动时触发的事件*/onScroll?: (e: Event) => void/*** 滚动结束触发的事件*/onStop?: (e: Event) => void/*** 滚动事件监听器的配置* @default {capture: false, passive: true}*/eventListenerOptions?: boolean | AddEventListenerOptions/*** 平滑滚动还是立即跳转* @default 'auto'*/behavior?: MaybeComputedRef<ScrollBehavior>
}/*** 我们必须检查滚动量是否足够接近某个阈值,以便更准确地计算arrivedState。* 这是因为scrollTop/scrollLeft是整数的数字,而scrollHeight/scrollWidth和clienttheight /clientWidth是四舍五入的。* https://developer.mozilla.org/enUS/docs/Web/API/Element/scrollHeight#determine_if_an_element_has_been_totally_scrolled*/
const ARRIVED_STATE_THRESHOLD_PIXELS = 1
- 再看一下代码实现,大量代码都是用来定义变量的。
/**
* 代码中一些函数的定义
*/
// 1 空函数
export const noop = () => {}// 2 防抖函数:如果多次调用,只有最后一次起作用,会在最后一次调用后,经过一段时候后触发回调。
useDebounceFn()// 3 节流函数:如果多次调用,那么在一个时间段内,只有第一次起作用。到了下个时间段,依旧只有第一次起作用。
useThrottleFn()
export function useScroll(element: MaybeComputedRef<HTMLElement | SVGElement | Window | Document | null | undefined>,options: UseScrollOptions = {},
) {/*** 处理用户传递的参数*/const {throttle = 0,idle = 200,onStop = noop,onScroll = noop,offset = {left: 0,right: 0,top: 0,bottom: 0,},eventListenerOptions = {capture: false,passive: true,},behavior = 'auto',} = options/*** x方向和y方向的偏移量。默认是0,也就是视口在页面左上角。*/const internalX = ref(0)const internalY = ref(0)/*** 提供给外部的计算属性。当用户设置x和y的时候,要出发滚动事件。* 在'scrollTo()'期间,不会在进程中触发额外的'scrollTo()'。*/const x = computed({get() {return internalX.value},set(x) {scrollTo(x, undefined)},})const y = computed({get() {return internalY.value},set(y) {scrollTo(undefined, y)},})/*** 使用 scrollTo 方法来滚动,传递目标位置的高度、左边距、滚动方式。*/function scrollTo(_x: number | undefined, _y: number | undefined) {const _element = resolveUnref(element)if (!_element)return(_element instanceof Document ? document.body : _element)?.scrollTo({top: resolveUnref(_y) ?? y.value,left: resolveUnref(_x) ?? x.value,behavior: resolveUnref(behavior),})}/*** 是否在滚动中*/const isScrolling = ref(false)/*** 四边的到达状态,默认左上角。*/const arrivedState = reactive({left: true,right: false,top: true,bottom: false,})/*** 朝着哪个方向滚动,默认哪边都不是。*/const directions = reactive({left: false,right: false,top: false,bottom: false,})/*** 滚动结束后,把滚动方向都设置为false。设置防抖时间:throttle + idle、* 同时调用用户传递的onStop*/const onScrollEnd = useDebounceFn((e: Event) => {isScrolling.value = falsedirections.left = falsedirections.right = falsedirections.top = falsedirections.bottom = falseonStop(e)}, throttle + idle)// ......// 最重要的滚动函数单独来看/*** 监听目标的scroll事件。如果发生了滚动,先看throttle的值* 如果设置了throttle,那么throttle的时间段内只调用一次。否则滚动即调用onScrollHandler。*/useEventListener(element,'scroll',throttle ? useThrottleFn(onScrollHandler, throttle, true, false) : onScrollHandler,eventListenerOptions,)return {x,y,isScrolling,arrivedState,directions,}
}
/**
* 滚动过程中触发的函数
*/
const onScrollHandler = (e: Event) => {const eventTarget = (e.target === document ? (e.target as Document).documentElement : e.target) as HTMLElementconst scrollLeft = eventTarget.scrollLeft// 如果滚动后的左边距小于原来的边距,说明时往左边滚动了。见下图1directions.left = scrollLeft < internalX.valuedirections.right = scrollLeft > internalY.value// 是否到达左边距。如果用户设置了offset.left,那么滚动后的左边距小于这个值就代表抵达到左边了arrivedState.left = scrollLeft <= 0 + (offset.left || 0)// 在从左往右滚动的过程中,这几个值变化如下图2// 可以简单理解,clientWidth是用户可见区域的宽度,scrollWidth是内容区真正的宽度。见下图3arrivedState.right= scrollLeft + eventTarget.clientWidth >= eventTarget.scrollWidth - (offset.right || 0) - ARRIVED_STATE_THRESHOLD_PIXELS// 实时更新滚动的距离internalX.value = scrollLeftlet scrollTop = eventTarget.scrollTop// 移动端兼容if (e.target === document && !scrollTop)scrollTop = document.body.scrollTop// 上下的处理和左右是一致的directions.top = scrollTop < internalY.valuedirections.bottom = scrollTop > internalY.valuearrivedState.top = scrollTop <= 0 + (offset.top || 0)arrivedState.bottom= scrollTop + eventTarget.clientHeight >= eventTarget.scrollHeight - (offset.bottom || 0) - ARRIVED_STATE_THRESHOLD_PIXELSinternalY.value = scrollTop// 滚动中状态设置为trueisScrolling.value = true// 不断触发scrollEnd事件,但是这个事件设置了防抖,只有在停顿超过throttle + idle时间后才会触发状态修改onScrollEnd(e)// 实时调用用户传递的函数onScroll(e)
}