功能
滚动至底部时,加载更多数据。
- 可以设置距离底部位置多少,触发加载更多功能
- 立即充满容器,自动执行加载更多功能
- 等到一定条件后,可以阻止加载更多
- 设置滚动节流时间
使用
以指令形式绑定元素,父元素或者自身需要添加overflow: auto
或者overflow: scroll
<template>
<div class="infinite-list-wrapper" style="overflow: auto">
<ul
v-infinite-scroll="load"
class="list"
:infinite-scroll-disabled="disabled"
>
<li v-for="i in count" :key="i" class="list-item">{{ i }}</li>
</ul>
<p v-if="loading">加载中</p>
<p v-if="noMore">没有更多了</p>
</div>
</template>
<script setup lang="ts">
let count = ref(2),
loading = ref(false)
const noMore = computed(() => {
return count.value >= 20
})
const disabled = computed(() => {
return loading.value || noMore.value
})
const load = () => {
loading.value = true
setTimeout(() => {
count.value += 2
loading.value = false
}, 1000)
}
</script>
属性名 | 说明 | 类型 | 可选值 | 默认值 |
---|---|---|---|---|
v-infinite-scroll | 滚动到底部时,加载更多数据 | function | -- | -- |
v-infinite-delay | 节流时延,单位为ms | number | -- | 200 |
v-infinite-distance | 触发加载的距离阈值,单位为px | number | -- | 0 |
v-infinite-immediate | 是否立即执行加载方法,以防初始状态下内容无法撑满容器。 | boolean | -- | true |
效果
加载开始
加载中
加载结束
原理
元素只有内部子元素的高度大于自身,并且自身的overflow
为auto/scroll
才可以发生滚动
- 找到带有属性
overflow
为auto/scroll
的上级元素,如果没有这个元素,直接停止 - 合并用户传递的属性与默认属性
- 如果有这个元素(container),使用
MutationObserver
监控元素,期间不断执行用户传递的函数,直到绑定 指令的元素与 container 的底部重叠 - container 发生滚动,执行用户传递的函数,直到触发
disabled
- 当页面卸载时,解除所有公共变量
实现
找到带有属性overflow
为auto/scroll
的上级元素 - getOverScrollEle
通过正则不断去匹配元素的overflow
属性,如果没有,就找父级元素,直到找到根元素
function getOverScrollEle(el: HTMLElement) {
let reg = /(scroll)|(auto)/g;
while (el != document.documentElement) {
let overflow = getComputedStyle(el).overflow
if (reg.test(overflow)) {
return el
} else {
if (el.parentElement) {
el = el.parentElement
} else {
el = document.documentElement
return
}
}
}
}
合并用户的属性与默认属性 -getScrollOptions
let defaultOption = {
"delay": 500,
"immediate": true,
"disabled": false,
"distance": 0,
}
function getScrollOptions(el: HTMLElement, instance: ComponentPublicInstance): defaultOptionKey<TypeDefaultOption> {
return Object.keys(defaultOption).reduce((map, key) => {
// 去除 infinite-scroll-
const attrVal = el.getAttribute(`infinite-scroll-${key}`) || ''
let value = instance[attrVal] ?? attrVal ?? defaultOption[key]
value = value === 'false' ? false : value
map[key] = value
return map
}, {})
}
因为是绑定在元素身上的属性,所以使用getAttribute
获取,同时获取的都是字符串
,需要对字符串false
做一次转换
instance
是 当前vue实例,类似于vue2
的this
,可以获取用户绑定的动态值
el
为绑定属性的节点
监控&自动执行
如果用户传递了 v-infinite-immediate
,需要立即执行用户传递的方法
let container = getOverScrollEle(el) as HTMLElement;
let { immediate } = getScrollOptions(el, instance!);
if (immediate) {
let observe = new MutationObserver(onScroll)
// subtree 可选
// 当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target
observe.observe(container, {
childList: true, // 儿子节点
subtree: true // 儿子的儿子
})
onScroll()
}
let onScroll = handleScroll.bind(null, el, cb)
使用MutationObserver
监控container(属性中有overflow)
,如果他的子元素或者子元素的子元素发生变化
,就要执行handleScroll
方法
handleScroll
主要作用是为了滚动时触发,判断元素是否触底,或者是否有disabled触发
触底的条件也十分简单,只要元素的被页面卷曲高度+元素的可视高度 == 元素的实际高度
就可以判断它已经到底了
function handleScroll(el: InfiniteScrollEl, fn: InfiniteScrollCallback) {
const { instance, observer, container } = el[SCOPE]
const { disabled, distance } = getScrollOptions(el, instance)
// // 说明没有触动
if (disabled) return;
// @ts-ignore
if (container.scrollTop + container.clientHeight + Number(distance) >= container.scrollHeight) {
console.log("触底")
fn()
}
}
container 元素滚动
当 container
元素滚动的时候,需要不断的执行onScroll
事件,由于是滚动事件,加上一个节流事件
,
当滚动的途中,不断的判断是否触底
container?.addEventListener("scroll", throttle(onScroll.bind(null, el, instance), delay))
function throttle(fn, delay = 200) {
let timer: null | NodeJS.Timeout = null
let flag = true
return () => {
if (!flag) return
flag = false
const args = arguments
timer = setTimeout(() => {
flag = true
clearTimeout(timer!)
fn.apply(window, args)
}, delay)
}
}
总结
graph TD
Start --> 寻找容器
寻找容器 -- overflow是auto/scroll--> 存在容器container
存在容器container-->shouldDoNow{是否需要立即执行}
shouldDoNow --N--> 绑定滚动事件
shouldDoNow--Y--> isDoNow[立即执行]
isDoNow-->MutationObserver监听元素container
isDoNow ---->onScroll
onScroll-->判断是否触底/disabled,执行用户传递函数
绑定滚动事件-->onScroll
源码
type infinite<S = string> = S extends `infinite-scroll-${infer P}` ? P : S;
type TypeDefaultOption = Record<`infinite-scroll-${string}`, any>
type defaultOptionKey<T> = {
[K in keyof T as infinite<K>]: T[K]
}
let defaultOption = {
"delay": 500,
"immediate": true,
"disabled": false,
"distance": 0,
}
function getScrollOptions(el: HTMLElement, instance: ComponentPublicInstance): defaultOptionKey<TypeDefaultOption> {
return Object.keys(defaultOption).reduce((map, key) => {
// 去除 infinite-scroll-
const attrVal = el.getAttribute(`infinite-scroll-${key}`) || ''
let value = instance[attrVal] ?? attrVal ?? defaultOption[key]
value = value === 'false' ? false : value
map[key] = value ?? defaultOption[`${key}`]
return map
}, {})
}
function getOverScrollEle(el: HTMLElement) {
let reg = /(scroll)|(auto)/g;
while (el != document.documentElement) {
let overflow = getComputedStyle(el).overflow
if (reg.test(overflow)) {
return el
} else {
if (el.parentElement) {
el = el.parentElement
} else {
el = document.documentElement
return
}
}
}
}
function throttle(fn, delay = 200) {
let timer: null | NodeJS.Timeout = null
let flag = true
return () => {
if (!flag) return
flag = false
const args = arguments
timer = setTimeout(() => {
flag = true
clearTimeout(timer!)
fn.apply(window, args)
}, delay)
}
}
const SCOPE = 'infinite-scroll'
type InfiniteScrollCallback = () => void
type InfiniteScrollEl = HTMLElement & {
[SCOPE]: {
container: HTMLElement | Window
containerEl: HTMLElement
instance: ComponentPublicInstance
delay: number
lastScrollTop: number
cb: InfiniteScrollCallback
onScroll: () => void
observer?: MutationObserver
}
}
// 滚动判断是否触底 或者 到了 disabled
function handleScroll(el: InfiniteScrollEl, fn: InfiniteScrollCallback) {
const { instance, observer, container } = el[SCOPE]
const { disabled, distance } = getScrollOptions(el, instance)
// // 说明没有触动
if (disabled) return;
// @ts-ignore
if (container.scrollTop + container.clientHeight + Number(distance) >= container.scrollHeight) {
fn()
} else {
if (observer) {
(observer as MutationObserver).disconnect()
delete el[SCOPE].observer
}
}
}
// 自定义指令
let vInfiniteScroll: ObjectDirective = {
async mounted(el, bindings) {
const { instance, value: cb } = bindings
let { delay, immediate } = getScrollOptions(el, instance!);
let container = getOverScrollEle(el) as HTMLElement;
let onScroll = handleScroll.bind(null, el, cb)
if (!instance) return
el[SCOPE] = {
container,
onScroll,
el,
instance,
}
if (immediate) {
let observe = new MutationObserver(onScroll)
el[SCOPE].observer = observe
// subtree 可选
// 当为 true 时,将会监听以 target 为根节点的整个子树。包括子树中所有节点的属性,而不仅仅是针对 target
observe.observe(container, {
childList: true, // 儿子节点
subtree: true // 儿子的儿子
})
onScroll()
}
container?.addEventListener("scroll", throttle(onScroll.bind(null, el, instance), delay))
},
unmounted(el) {
const { onScroll, container } = el[SCOPE]
if (container) {
container.removeEventListener("scroll", onScroll)
el[SCOPE] = {}
}
}
}