vue3写一个触底加载hook
天行健,地势坤 -- 《周易·象传》
背景:在一个滚动容器下实现触底加载更多的函数,由于此模块使用的场景较多,因而自己实现了一个滚动加载的hook(顺带尝试了下hook)
需求:左侧tab切换时,右侧聊天历史tab要清空并重新请求;右侧触底时,要触发分页请求(如果有);使用dayjs中的fromnow
方法以精确体现用户在今日对话中的记录(略)。
- 基础知识引入
如何让一个容器触发滚动态:
一句话总结:容器的内容物高度大于容器本身高度且容器的css样式为overflow:auto
时触发。
如何给一个容器添加滚动事件:
element.addEventListener('scroll', scrollFn)
- 代码实现
useBottomScroll.ts
import { nextTick, onMounted, onUnmounted, ref } from 'vue'
/**
* elementRef : 传入的dom实例
* onReachBottomFn :滚动加载函数
* reachBottomDistance : 距离底部多少距离时触发
*/
function useScrollBottom(elementRef, onReachBottomFn, reachBottomDistance = 100) {
const isReachBottom = ref(false)
const onScroll = async () => {
const scrollTop = elementRef.value.scrollTop
const scrollHeight = elementRef.value.scrollHeight
const clientHeight = elementRef.value.clientHeight
const currentHeight = scrollTop + clientHeight
const isAtBottom = currentHeight + reachBottomDistance >= scrollHeight
if (isAtBottom && !isReachBottom.value) {
// 调用传入的触底触发函数 bool 值代表是否还有更多数据
const isEnd = await onReachBottomFn()
isReachBottom.value = isEnd
}
}
onMounted(async () => {
nextTick(async () => {
elementRef.value.addEventListener('scroll', onScroll)
// 默认触发一次分页接口
await onReachBottomFn()
})
})
onUnmounted(() => {
elementRef.value && elementRef.value.removeEventListener('scroll', onScroll)
})
return {
isReachBottom
}
}
export default useScrollBottom
这里最重要的有两点,
(1). 判断滚动的时机
滚动的时机以isAtBottom变量划定,即当前位置加上预设距离是否大于等于元素总高,如果是,则为true
(2). 修改滚动的条件:isReachBottom
修改滚动的条件isReachBottom则由调用方提供,本质上是当前页数current*当前条数size 是否大于等于 总条数total,每次在条件满足触发加载后都会重新赋值isReachBottom
- 业务调用:
<div ref="todayRef" class="scroll-container">
<!--balabala-->
</div>
import useScrollBottom from '@/hooks/useBottomScroll.ts'
// 监听左侧tag页,一旦切换即重新触发
watch(
() => currentTag.value,
(n) => {
todayBool.value = false
todayList.value = []
todayPage.value.current = 0
loadMoreDataFn()
},
{
immediate: true
}
)
const todayList = ref([])
const todayRef = ref(null)
// 这里最重要的就是current size默认写死,total可以扔掉
const todayPage = ref({ size: 10, current: 0, total: 0 } )
const { isReachBottom: todayBool } = useScrollBottom(todayRef, loadMoreDataFn)
const loadMoreDataFn= async () => {
todayPage.value.current += 1
const params = {
current: todayPage.value.current,
size: 10,
}
const res = await getTodayStoryListApi(params)
if (Array.isArray(res.records)) {
todayList.value = [...todayList.value, ...res.records]
}
// 返回的isEnd参数-》params.current * params.size >= Number(res.total))
return new Promise((c) => c(params.current * params.size >= Number(res.total)))
}
- 总结与分析
滚动加载最重要的其实就是current参数,不论tab页的切换亦或者触底后是否继续访问接口都与此息息相关,笔者原本准备将请求函数写在hook内部,但是如此一来就丧失了一些灵活性——固定输入,并且如果有时候在外部需要一些返回值时,又不方便提取(也可能是刚开始用hook),因而就将loadFn写在了外部,而触发的时机本质上有两个点:1,是否接近或者等于约定值,2. 外部传入的isEnd值是否为false,而isEnd又是外部传入的current*size >= total返回的布尔值,一套流程下来也就形成了闭环。
- 不足
之前仿照别的项目组时的做法,想在loadMoreDataFn内赋一个默认值isRefresh 来判断是否是否切换了左侧tab,但是测试后发现无效,猜测可能是在hook内部调用
const isEnd = await onReachBottomFn()
时没能将参数传递进去,这里百思不得其解,因而不得不在业务模块使用watch
手动控制current和todayList的值,如有知道解决办法的也欢迎指教,不胜感激
以上。