大家好,我是鱼樱!!!
关注公众号【鱼樱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>
性能优化要点
-
减少响应式对象数量:
- 使用单一的
reactive
对象 管理所有内部状态,而不是多个ref
- 只对真正需要响应式的数据进行跟踪
- 使用单一的
-
避免不必要的计算和渲染:
- 下拉/上拉区域使用
v-show
而非v-if
,减少 DOM 操作 - 减少了模板中的插槽数量,简化组件结构
- 下拉/上拉区域使用
-
优化渲染流程:
- 使用
nextTick
配合延迟触发更新,避免状态变化引起的连锁渲染 - 在
finishRefresh
和finishLoadMore
中延迟调用refresh
,防止渲染阻塞
- 使用
-
简化 API 设计:
- 专注于最常用的功能,减少配置项数量
- 使用直观的命名约定,减少调用者的学习成本
-
滚动优化:
- 只在需要监听滚动时才设置较高的
probeType
- 减少了滚动监听回调中的逻辑处理
- 只在需要监听滚动时才设置较高的
核心优势
-
极简调用:
- 父组件只需传入极少数必要参数
- 无需关心内部实现,只需处理
refresh
和loadMore
事件
-
性能优先:
- 减少了不必要的响应式追踪和 DOM 操作
- 精简了组件结构和渲染流程
-
合理的默认行为:
- 提供合理的默认文本和行为
- 可通过少量的 props 进行自定义
这个优化版本在保持功能完整的同时,显著简化了使用方式,并通过多种手段优化了性能表现,适合在大型应用中使用。