以下是手写 useInfiniteScroll
函数的实现,用于实现无限滚动加载功能:
typescript
import {
ref,
watch,
onUnmounted,
unref,
type Ref,
type ComponentPublicInstance
} from 'vue'
interface UseInfiniteScrollOptions {
direction?: 'vertical' | 'horizontal'
interval?: number
immediate?: boolean
}
export function useInfiniteScroll(
target: Ref<HTMLElement | ComponentPublicInstance | Window | null>,
onLoad: () => Promise<void> | void,
options: UseInfiniteScrollOptions = {}
) {
const {
direction = 'vertical',
interval = 100,
immediate = true
} = options
const isLoading = ref(false)
const isFinished = ref(false)
let lastTriggerTime = 0
const getElement = (): HTMLElement | Window => {
const el = unref(target)
if (el instanceof Window) return window
return (el as ComponentPublicInstance)?.$el ?? el
}
const checkInView = () => {
if (isLoading.value || isFinished.value) return
const el = getElement()
if (!el || el instanceof Window) return checkWindowScroll()
if (direction === 'vertical') {
const { scrollTop, scrollHeight, clientHeight } = el as HTMLElement
return scrollTop + clientHeight >= scrollHeight - 50
} else {
const { scrollLeft, scrollWidth, clientWidth } = el as HTMLElement
return scrollLeft + clientWidth >= scrollWidth - 50
}
}
const checkWindowScroll = () => {
const { documentElement } = document
if (direction === 'vertical') {
return (
window.innerHeight + documentElement.scrollTop >=
documentElement.scrollHeight - 50
)
} else {
return (
window.innerWidth + documentElement.scrollLeft >=
documentElement.scrollWidth - 50
)
}
}
const handleScroll = () => {
if (Date.now() - lastTriggerTime < interval) return
if (!checkInView()) return
lastTriggerTime = Date.now()
triggerLoad()
}
const triggerLoad = async () => {
if (isLoading.value || isFinished.value) return
isLoading.value = true
try {
const result = await onLoad()
if (result === false) isFinished.value = true
} finally {
isLoading.value = false
}
}
const addListener = () => {
const el = getElement()
if (el instanceof Window) {
window.addEventListener('scroll', handleScroll)
} else if (el) {
el.addEventListener('scroll', handleScroll)
}
}
const removeListener = () => {
const el = getElement()
if (el instanceof Window) {
window.removeEventListener('scroll', handleScroll)
} else if (el) {
el.removeEventListener('scroll', handleScroll)
}
}
watch(
() => getElement(),
(newEl, oldEl) => {
removeListener()
addListener()
},
{ immediate: true }
)
const load = () => triggerLoad()
onUnmounted(() => {
removeListener()
})
// Immediate load
if (immediate) {
setTimeout(triggerLoad, 0)
}
return {
isLoading,
isFinished,
load
}
}
使用示例:
vue
<template>
<div ref="container" class="scroll-container">
<div v-for="item in items" :key="item.id">{{ item.content }}</div>
<div v-if="isLoading">Loading...</div>
<div v-if="isFinished">No more data</div>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { useInfiniteScroll } from './useInfiniteScroll'
const container = ref<HTMLElement | null>(null)
const items = ref<any[]>([])
let page = 1
const { isLoading, isFinished, load } = useInfiniteScroll(
container,
async () => {
const newData = await fetchData(page)
if (newData.length === 0) return false
items.value.push(...newData)
page++
},
{ immediate: true }
)
async function fetchData(p: number) {
// 模拟API请求
return new Array(10).fill(null).map((_, i) => ({
id: (p - 1) * 10 + i,
content: `Item ${(p - 1) * 10 + i + 1}`
}))
}
</script>
<style>
.scroll-container {
height: 500px;
overflow-y: auto;
}
</style>
实现细节说明:
-
核心功能:
- 自动检测滚动到底部时触发加载
- 支持垂直/水平滚动方向
- 内置防抖机制(通过interval参数控制)
-
响应式状态:
isLoading
: 加载状态指示isFinished
: 数据是否全部加载完成
-
滚动检测逻辑:
- 自动适配普通元素和window的滚动容器
- 智能计算滚动位置(距底部50px触发)
- 支持自定义检测间隔(默认100ms)
-
生命周期管理:
- 自动清理事件监听器
- 支持动态切换目标元素
- 组件卸载时自动移除监听
-
扩展功能:
- 支持立即执行首次加载(immediate参数)
- 提供手动触发方法(load())
- 支持通过返回false终止加载
-
类型安全:
- 完整TypeScript类型支持
- 兼容Vue组件实例和原生DOM元素
- 支持Window对象的滚动监听
参数说明:
参数 | 类型 | 说明 |
---|---|---|
target | Ref<HTMLElement | Window> | 滚动容器元素引用 |
onLoad | () => Promise | void | 加载回调函数 |
options | UseInfiniteScrollOptions | 配置选项 |
选项参数:
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
direction | 'vertical'/'horizontal' | 'vertical' | 滚动方向 |
interval | number | 100 | 触发间隔(毫秒) |
immediate | boolean | true | 是否立即执行首次加载 |
该实现提供了与VueUse相似的功能,同时保留了以下特性:
- 自动处理组件卸载时的资源清理
- 完善的TypeScript类型支持
- 灵活的滚动容器支持(包括Vue组件实例)
- 可配置的触发策略和加载控制