前言
如何实现滚动至底部时,加载更多数据?原理是什么?下面一起来探究一下~
组件介绍
- 功能 滚动至底部时,加载更多数据。
- 属性
属性 | 说明 | 类型 | 默认 |
---|---|---|---|
v-infinite-scroll | 滚动到底部时,加载更多数据 | Function | — |
infinite-scroll-disabled | 是否禁用 | boolean | false |
infinite-scroll-delay | 节流时延,单位为ms | number | 200 |
infinite-scroll-distance | 触发加载的距离阈值,单位为px | number | 0 |
infinite-scroll-immediate | 是否立即执行加载方法,以防初始状态下内容无法撑满容器。 | boolean | true |
- 效果预览
源码下载
git clone https://github.com/element-plus/element-plus.git
cd element-plus
pnpm install
// 本地打开文档
pnpm docs:dev
// 本地运行例子
pnpm run dev
调试源码
- 将官网例子复制到
play\src\App.vue
,参考代码如下:
<template>
<div class="play-container">
<ul v-infinite-scroll="load" class="infinite-list" style="overflow: auto">
<li v-for="i in count" :key="i" class="infinite-list-item">{{ i }}</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
const load = () => {
count.value += 2
}
</script>
- 打开源码文件所在位置
packages\components\infinite-scroll\index.ts
,并打debugger - 运行
pnpm run dev
并打开http://localhost:3000/
,效果如下:
通过调试可以发现,InfiniteScroll具有mounted、unmounted、updated等钩子,且有如下属性及方法,接着我们通过源码来看一下这些属性及方法具体有什么作用,探究一下到底为什么通过指令就能实现无限滚动~
源码分析
index.ts 入口文件
// 文件位置packages\components\infinite-scroll\index.ts
import InfiniteScroll from './src'
import type { App } from 'vue'
import type { SFCWithInstall } from '@element-plus/utils'
const _InfiniteScroll = InfiniteScroll as SFCWithInstall<typeof InfiniteScroll>
_InfiniteScroll.install = (app: App) => {
app.directive('InfiniteScroll', _InfiniteScroll)
}
export default _InfiniteScroll
export const ElInfiniteScroll = _InfiniteScroll
入口文件的作用很简单,就是利用app.directive()注册一个全局指令【同时传递一个名字和一个指令定义】
infinite-scroll\src\index.ts 功能文件
我们可以在ts文件看不懂的地方打debugger,哪里不会点哪里
- getScrollOptions
import type { ComponentPublicInstance } from 'vue'
export const DEFAULT_DELAY = 200
export const DEFAULT_DISTANCE = 0
const attributes = {
delay: {
type: Number,
default: DEFAULT_DELAY,
},
distance: {
type: Number,
default: DEFAULT_DISTANCE,
},
disabled: {
type: Boolean,
default: false,
},
immediate: {
type: Boolean,
default: true,
},
}
type Attrs = typeof attributes
type ScrollOptions = { [K in keyof Attrs]: Attrs[K]['default'] }
const getScrollOptions = (
el: HTMLElement,
instance: ComponentPublicInstance
): ScrollOptions => {
return Object.entries(attributes).reduce((acm, [name, option]) => {
const { type, default: defaultValue } = option
const attrVal = el.getAttribute(`infinite-scroll-${name}`)
let value = instance[attrVal] ?? attrVal ?? defaultValue
value = value === 'false' ? false : value
value = type(value)
acm[name] = Number.isNaN(value) ? defaultValue : value
return acm
}, {} as ScrollOptions)
}
getScrollOptions
函数接收两个实参,分别是指令绑定的元素el以及触发指令的组件实例,作用就是获取默认属性选项,这里咱们可以借鉴学习的点是ts的keyof遍历属性【type ScrollOptions = { [K in keyof Attrs]: Attrs[K]['default'] }
】以及利用Object.entries
返回参数对象自身的(不含继承的)所有可遍历(enumerable)属性的键值对数组继而遍历对象~
- getScrollContainer
declare const isClient: boolean;
export const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
if (!isClient) return false
// 非空断言
const key = (
{
undefined: 'overflow',
true: 'overflow-y',
false: 'overflow-x',
} as const
)[String(isVertical)]!
// overflow = 'auto'
const overflow = getStyle(el, key)
return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
}
export const getScrollContainer = (
el: HTMLElement,
isVertical?: boolean
): Window | HTMLElement | undefined => {
if (!isClient) return
let parent: HTMLElement = el
while (parent) {
if ([window, document, document.documentElement].includes(parent))
return window
if (isScroll(parent, isVertical)) return parent
parent = parent.parentNode as HTMLElement
}
return parent
}
getScrollContainer
的作用是获取滚动的容器元素,主要是通过判断元素的overflow属性是否为scroll|auto|overlay
来判断
- handleScroll
export const SCOPE = 'ElInfiniteScroll'
type InfiniteScrollCallback = () => void
import type { ComponentPublicInstance } from 'vue'
type InfiniteScrollEl = HTMLElement & {
[SCOPE]: {
container: HTMLElement | Window
containerEl: HTMLElement
instance: ComponentPublicInstance
delay: number // export for test
lastScrollTop: number
cb: InfiniteScrollCallback
onScroll: () => void
observer?: MutationObserver
}
}
// 获取当前元素距离顶部的距离
export const getOffsetTop = (el: HTMLElement) => {
let offset = 0
let parent = el
while (parent) {
offset += parent.offsetTop
parent = parent.offsetParent as HTMLElement
}
return offset
}
// 获取指定容器距离顶部距离之差
export const getOffsetTopDistance = (
el: HTMLElement,
containerEl: HTMLElement
) => {
return Math.abs(getOffsetTop(el) - getOffsetTop(containerEl))
}
// 控制滚动
const handleScroll = (el: InfiniteScrollEl, cb: InfiniteScrollCallback) => {
const { container, containerEl, instance, observer, lastScrollTop } =
el[SCOPE]
const { disabled, distance } = getScrollOptions(el, instance)
const { clientHeight, scrollHeight, scrollTop } = containerEl
const delta = scrollTop - lastScrollTop
el[SCOPE].lastScrollTop = scrollTop
// trigger only if full check has done and not disabled and scroll down
if (observer || disabled || delta < 0) return
let shouldTrigger = false
if (container === el) {
shouldTrigger = scrollHeight - (clientHeight + scrollTop) <= distance
} else {
// get the scrollHeight since el might be visible overflow
const { clientTop, scrollHeight: height } = el
const offsetTop = getOffsetTopDistance(el, containerEl)
shouldTrigger =
scrollTop + clientHeight >= offsetTop + clientTop + height - distance
}
if (shouldTrigger) {
cb.call(instance)
}
}
handleScroll
函数其实就是触底判断然后控制是否继续滚动触发加载,这里有使用节流函数throttle来稀释函数的执行频率,这里的计算会涉及到元素的以下属性:
- scrollHeight【元素内容高度的度量,包括由于溢出导致的视图中不可见内容】
- clientHeight【元素内部的高度(以像素为单位),包含内边距】
- scrollTop【元素的内容垂直滚动的像素数】
- clientTop【一个元素顶部边框的宽度】
- offsetTop【当前元素相对于其
offsetParent
元素的顶部内边距的距离】
- checkFull
function checkFull(el: InfiniteScrollEl, cb: InfiniteScrollCallback) {
const { containerEl, instance } = el[SCOPE]
const { disabled } = getScrollOptions(el, instance)
if (disabled || containerEl.clientHeight === 0) return
if (containerEl.scrollHeight <= containerEl.clientHeight) {
cb.call(instance)
} else {
destroyObserver(el)
}
}
checkFull函数的作用是实现当元素还没滚动到底部时立即触发加载函数,否则停止监听目标
总结
看完以上函数,不难总结出无限滚动的实现主要是通过判断是否触底来触发加载,该指令在mounted钩子时添加滚动事件及目标元素的监听,updated钩子执行加载函数,并在unmounted时停止监听目标元素及滚动事件~