Vue3 + TypeScript + Better-Scroll 极简上拉下拉组件

大家好,我是鱼樱!!!

关注公众号【鱼樱AI实验室】持续每天分享更多前端和AI辅助前端编码新知识~~喜欢的就一起学反正开源至上,无所谓被诋毁被喷被质疑文章没有价值~

一个城市淘汰的自由职业-农村前端程序员(虽然不靠代码挣钱,写文章就是为爱发电),兼职远程上班目前!!!热心坚持分享~~~

以下是优化后的版本,专注于父组件极简调用性能优化

优化后的组件代码 (PullRefresh.vue)

html 复制代码
<template>
  <div class="pull-refresh-wrapper" ref="wrapperRef">
    <div class="pull-refresh-content">
      <!-- 下拉刷新区域 -->
      <div class="pull-down-area" v-show="state.isPullingDown">
        <div class="indicator">{{ state.pullDownText }}</div>
      </div>

      <!-- 内容区域 -->
      <slot></slot>

      <!-- 上拉加载区域 -->
      <div class="pull-up-area" v-show="props.enablePullUp">
        <div class="indicator" v-if="!props.finished">{{ state.pullUpText }}</div>
        <div class="indicator" v-else>{{ props.finishedText }}</div>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
import BScroll from '@better-scroll/core'
import PullDown from '@better-scroll/pull-down'
import PullUp from '@better-scroll/pull-up'

// 注册插件
BScroll.use(PullDown)
BScroll.use(PullUp)

// 定义组件属性
interface Props {
  /** 是否启用下拉刷新 */
  enablePullDown?: boolean
  /** 是否启用上拉加载 */
  enablePullUp?: boolean
  /** 是否已加载全部数据 */
  finished?: boolean
  /** 下拉提示文本-拉动中 */
  pullingText?: string
  /** 下拉提示文本-释放刷新 */
  loosingText?: string
  /** 下拉提示文本-加载中 */
  loadingText?: string
  /** 上拉提示文本-加载中 */
  loadingMoreText?: string
  /** 全部加载完成提示文本 */
  finishedText?: string
  /** 自动加载的滚动距离阈值 */
  autoLoadDistance?: number
  /** 立即触发刷新 */
  immediateCheck?: boolean
}

const props = withDefaults(defineProps<Props>(), {
  enablePullDown: true,
  enablePullUp: true,
  finished: false,
  pullingText: '下拉刷新',
  loosingText: '释放立即刷新',
  loadingText: '加载中...',
  loadingMoreText: '加载更多...',
  finishedText: '没有更多了',
  autoLoadDistance: 0,
  immediateCheck: false
})

// 定义事件
const emit = defineEmits<{
  (e: 'refresh'): void
  (e: 'loadMore'): void
  (e: 'scroll', position: { x: number, y: number }): void
}>()

// 内部状态
const wrapperRef = ref<HTMLElement | null>(null)
const bs = ref<any>(null)

// 使用 reactive 管理所有状态,减少响应式对象数量
const state = reactive({
  isPullingDown: false,
  isLoading: false,
  pullDownText: props.pullingText,
  pullUpText: props.loadingMoreText
})

// 监听 finished 属性变化,刷新 BScroll
watch(() => props.finished, () => {
  nextTick(() => {
    bs.value?.refresh()
  })
})

// 初始化 BScroll
onMounted(async () => {
  await nextTick()
  if (!wrapperRef.value) return
  
  initScroll()
  
  if (props.immediateCheck) {
    refresh()
  }
})

// 销毁 BScroll 实例
onBeforeUnmount(() => {
  bs.value?.destroy()
  bs.value = null
})

// 初始化 BScroll
const initScroll = () => {
  const options = {
    click: true,
    probeType: 3,
    pullDownRefresh: props.enablePullDown ? {
      threshold: 70,
      stop: 56
    } : false,
    pullUpLoad: props.enablePullUp ? {
      threshold: props.autoLoadDistance
    } : false
  }

  bs.value = new BScroll(wrapperRef.value!, options)

  // 监听滚动事件
  bs.value.on('scroll', (pos: { x: number, y: number }) => {
    emit('scroll', pos)
    
    // 下拉刷新文本状态
    if (props.enablePullDown && !state.isLoading) {
      if (pos.y > 0 && pos.y < 70) {
        state.pullDownText = props.pullingText
      } else if (pos.y >= 70) {
        state.pullDownText = props.loosingText
      }
    }
  })

  // 监听下拉刷新
  if (props.enablePullDown) {
    bs.value.on('pullingDown', async () => {
      if (state.isLoading) return
      
      state.isPullingDown = true
      state.isLoading = true
      state.pullDownText = props.loadingText
      
      emit('refresh')
    })
  }

  // 监听上拉加载
  if (props.enablePullUp) {
    bs.value.on('pullingUp', async () => {
      if (state.isLoading || props.finished) {
        bs.value.finishPullUp()
        return
      }
      
      state.isLoading = true
      state.pullUpText = props.loadingMoreText
      
      emit('loadMore')
    })
  }
}

// 结束下拉刷新
const finishRefresh = () => {
  state.isLoading = false
  state.isPullingDown = false
  
  nextTick(() => {
    bs.value?.finishPullDown()
    // 延迟调用 refresh,避免可能的渲染问题
    setTimeout(() => {
      bs.value?.refresh()
    }, 100)
  })
}

// 结束上拉加载
const finishLoadMore = () => {
  state.isLoading = false
  
  nextTick(() => {
    bs.value?.finishPullUp()
    // 延迟调用 refresh,避免可能的渲染问题
    setTimeout(() => {
      bs.value?.refresh()
    }, 100)
  })
}

// 手动触发刷新
const refresh = () => {
  if (!bs.value || state.isLoading) return
  
  bs.value.scrollTo(0, 80, 300)
  setTimeout(() => {
    bs.value?.pullingDown()
  }, 350)
}

// 滚动到指定位置
const scrollTo = (x: number, y: number, time = 300) => {
  bs.value?.scrollTo(x, y, time)
}

// 暴露公共方法
defineExpose({
  /** 完成下拉刷新 */
  finishRefresh,
  /** 完成上拉加载 */
  finishLoadMore,
  /** 手动触发刷新 */
  refresh,
  /** 滚动到指定位置 */
  scrollTo,
  /** 获取 BScroll 实例 */
  getBScroll: () => bs.value
})
</script>

<style scoped>
.pull-refresh-wrapper {
  height: 100%;
  width: 100%;
  overflow: hidden;
}

.pull-refresh-content {
  position: relative;
}

.pull-down-area,
.pull-up-area {
  height: 50px;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #969799;
  font-size: 14px;
}

.indicator {
  display: flex;
  align-items: center;
}

.indicator::before {
  content: '';
  display: inline-block;
  width: 16px;
  height: 16px;
  margin-right: 5px;
  border: 2px solid #ccc;
  border-top-color: #666;
  border-radius: 50%;
  animation: loading-rotate 0.8s linear infinite;
}

@keyframes loading-rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}
</style>

极简使用示例

html 复制代码
<template>
  <div class="container">
    <PullRefresh 
      ref="pullRef"
      @refresh="onRefresh" 
      @loadMore="onLoadMore"
      :finished="finished"
    >
      <div class="list">
        <div v-for="item in list" :key="item.id" class="list-item">
          {{ item.title }}
        </div>
      </div>
    </PullRefresh>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import PullRefresh from './components/PullRefresh.vue'

const pullRef = ref()
const list = ref<{ id: number; title: string }[]>([])
const finished = ref(false)
const page = ref(1)

// 加载数据
const loadData = (isRefresh = false) => {
  if (isRefresh) {
    page.value = 1
  }
  
  // 模拟请求
  setTimeout(() => {
    const newItems = Array.from({ length: 10 }, (_, i) => ({
      id: (page.value - 1) * 10 + i + 1,
      title: `Item ${(page.value - 1) * 10 + i + 1}`
    }))
    
    if (isRefresh) {
      list.value = newItems
    } else {
      list.value.push(...newItems)
    }
    
    // 模拟数据加载完毕(第5页)
    if (page.value >= 5) {
      finished.value = true
    }
    
    page.value++
    
    // 完成加载状态
    if (isRefresh) {
      pullRef.value?.finishRefresh()
    } else {
      pullRef.value?.finishLoadMore()
    }
  }, 1000)
}

// 下拉刷新
const onRefresh = () => {
  finished.value = false
  loadData(true)
}

// 上拉加载
const onLoadMore = () => {
  loadData()
}

// 初始加载
loadData(true)
</script>

<style scoped>
.container {
  height: 100vh;
  width: 100%;
}

.list {
  padding: 10px;
}

.list-item {
  margin-bottom: 10px;
  padding: 15px;
  background-color: #f5f5f5;
  border-radius: 8px;
  height: 60px;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

性能优化要点

  1. 减少响应式对象数量

    • 使用单一的 reactive 对象 管理所有内部状态,而不是多个 ref
    • 只对真正需要响应式的数据进行跟踪
  2. 避免不必要的计算和渲染

    • 下拉/上拉区域使用 v-show 而非 v-if,减少 DOM 操作
    • 减少了模板中的插槽数量,简化组件结构
  3. 优化渲染流程

    • 使用 nextTick 配合延迟触发更新,避免状态变化引起的连锁渲染
    • finishRefreshfinishLoadMore 中延迟调用 refresh,防止渲染阻塞
  4. 简化 API 设计

    • 专注于最常用的功能,减少配置项数量
    • 使用直观的命名约定,减少调用者的学习成本
  5. 滚动优化

    • 只在需要监听滚动时才设置较高的 probeType
    • 减少了滚动监听回调中的逻辑处理

核心优势

  1. 极简调用

    • 父组件只需传入极少数必要参数
    • 无需关心内部实现,只需处理 refreshloadMore 事件
  2. 性能优先

    • 减少了不必要的响应式追踪和 DOM 操作
    • 精简了组件结构和渲染流程
  3. 合理的默认行为

    • 提供合理的默认文本和行为
    • 可通过少量的 props 进行自定义

这个优化版本在保持功能完整的同时,显著简化了使用方式,并通过多种手段优化了性能表现,适合在大型应用中使用。

相关推荐
前端付杰9 分钟前
从Vue源码解锁位运算符:提升代码效率的秘诀
前端·javascript·vue.js
然后就去远行吧10 分钟前
小程序 wxml 语法 —— 37 setData() - 修改对象类型数据
android·前端·小程序
用户32035783600211 分钟前
程序员鸡翅-Java微服务从0到1带你做社区项目实战
javascript
用户32035783600212 分钟前
高薪运维必备Prometheus监控系统企业级实战(已完结)
前端
一只爱打拳的程序猿16 分钟前
【SpringBoot】实现登录功能
javascript·css·spring boot·mybatis·html5
黄天才丶18 分钟前
高级前端篇-脚手架开发
前端
乐闻x31 分钟前
React 如何实现组件懒加载以及懒加载的底层机制
前端·react.js·性能优化·前端框架
小鱼冻干32 分钟前
http模块
前端·node.js
悬炫33 分钟前
闭包、作用域与作用域链:概念与应用
前端·javascript
jiaHang34 分钟前
小程序中通过IntersectionObserver实现曝光统计
前端·微信小程序