手写VueUse的useInfiniteScroll函数实现

以下是手写 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>

实现细节说明:

  1. 核心功能

    • 自动检测滚动到底部时触发加载
    • 支持垂直/水平滚动方向
    • 内置防抖机制(通过interval参数控制)
  2. 响应式状态

    • isLoading: 加载状态指示
    • isFinished: 数据是否全部加载完成
  3. 滚动检测逻辑

    • 自动适配普通元素和window的滚动容器
    • 智能计算滚动位置(距底部50px触发)
    • 支持自定义检测间隔(默认100ms)
  4. 生命周期管理

    • 自动清理事件监听器
    • 支持动态切换目标元素
    • 组件卸载时自动移除监听
  5. 扩展功能

    • 支持立即执行首次加载(immediate参数)
    • 提供手动触发方法(load())
    • 支持通过返回false终止加载
  6. 类型安全

    • 完整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组件实例)
  • 可配置的触发策略和加载控制
相关推荐
0思必得07 小时前
[Web自动化] Selenium处理动态网页
前端·爬虫·python·selenium·自动化
东东5168 小时前
智能社区管理系统的设计与实现ssm+vue
前端·javascript·vue.js·毕业设计·毕设
catino8 小时前
图片、文件的预览
前端·javascript
layman052810 小时前
webpack5 css-loader:从基础到原理
前端·css·webpack
半桔10 小时前
【前端小站】CSS 样式美学:从基础语法到界面精筑的实战宝典
前端·css·html
AI老李10 小时前
PostCSS完全指南:功能/配置/插件/SourceMap/AST/插件开发/自定义语法
前端·javascript·postcss
_OP_CHEN10 小时前
【前端开发之CSS】(一)初识 CSS:网页化妆术的终极指南,新手也能轻松拿捏页面美化!
前端·css·html·网页开发·样式表·界面美化
啊哈一半醒10 小时前
CSS 主流布局
前端·css·css布局·标准流 浮动 定位·flex grid 响应式布局
PHP武器库10 小时前
ULUI:不止于按钮和菜单,一个专注于“业务组件”的纯 CSS 框架
前端·css
电商API_1800790524710 小时前
第三方淘宝商品详情 API 全维度调用指南:从技术对接到生产落地
java·大数据·前端·数据库·人工智能·网络爬虫