背景
在长列表展示时,基于移动端的屏幕尺寸考虑,很少会有PC端的那种翻页交互设计,而且考虑到页面性能,也不可能一次加载完全部数据,取而代之的方案是在移动端的列表中,通过上拉加载更多数据。
已经有很多组件库实现这个基础功能,比如 antd-mobile@2 中的 ListView ,但是一般配置项较多,使用起来较为繁琐。
原理
其实“加载更多”的原理也比较简单,首先获取列表的总长度和第一页的数据,如果第一页的数据长度小于列表的总长度,就在列表的底部增加“正在加载...”的元素,监听页面的滚动事件,如果底部的“正在加载...”元素出现在可视区域,我们就请求下一页的数据,并继续监听页面的滚动事件,直到当前列表展示数据等于列表的总长度。
所以主要的技术点是:怎么判断底部的“正在加载...”元素出现在了可视区域?
对于这个问题,大家可能首先想到的是监听页面滚动,然后动态计算底部的“正在加载...”元素位置。
监听页面滚动
移动端监听页面滚动很简单,以react为例:
useEffect(() => {
function onTouchMove() {
// do something...
}
document.addEventListener('touchmove', onTouchMove)
return () => {
document.removeEventListener('touchmove', onTouchMove)
}
}, [])
复制代码
PS:滚动的监听一般还需要考虑加上节流函数。
或者可以使用 requestAnimationFrame
不停计算,取代对滚动的监听,对这个API不了解的同学,可以自行查阅MDN:
const requestRef = useRef()
const scrollCb = () => {
// do something...
requestRef.current = requestAnimationFrame(scrollCb)
}
useEffect(() => {
requestRef.current = requestAnimationFrame(scrollCb)
return () => cancelAnimationFrame(requestRef.current)
}, [])
复制代码
可视区域检测
底部的“正在加载...”元素是否出现在可视区域,我们一般是有三种方案:
- offsetTop、scrollTop
offsetTop
:元素的上外边框至包含元素的上内边框之间的像素距离。
scrollTop
:一个元素的 scrollTop
值是这个元素的内容顶部(卷起来的)到它的视口可见内容(的顶部)的距离的度量。当一个元素的内容没有产生垂直方向的滚动条,那么它的 scrollTop
值为0
。
function isInViewPortOfOne (el) {
const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight
const offsetTop = el.offsetTop
const scrollTop = document.documentElement.scrollTop
const top = offsetTop - scrollTop
return top <= viewPortHeight
}
复制代码
- getBoundingClientRect
Element.getBoundingClientRect()
方法返回元素的大小及其相对于视口的位置。MDN
function isInViewPort(element) {
const viewWidth = window.innerWidth || document.documentElement.clientWidth;
const viewHeight = window.innerHeight || document.documentElement.clientHeight;
const {
top,
right,
bottom,
left,
} = element.getBoundingClientRect();
return (
top >= 0 &&
left >= 0 &&
right <= viewWidth &&
bottom <= viewHeight
);
}
复制代码
- Intersection Observer
Intersection Observer
交叉观察器,从这个命名就可以看出它用于判断两个元素是否重叠,因为不用进行事件的监听,性能方面相比getBoundingClientRect
会好很多。
它的用法非常简单。
var io = new IntersectionObserver(callback, option);
复制代码
上面代码中,IntersectionObserver
是浏览器原生提供的构造函数,接受两个参数:callback
是可见性变化时的回调函数,option
是配置对象(该参数可选)。
构造函数的返回值是一个观察器实例。实例的observe
方法可以指定观察哪个 DOM 节点。
// 开始观察
io.observe(document.getElementById('example'));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
复制代码
上面代码中,observe
的参数是一个 DOM 节点对象。如果要观察多个节点,就要多次调用这个方法。
io.observe(elementA);
io.observe(elementB);
复制代码
推荐方案和实现
通过上面的介绍,我们能得出结论:使用Intersection Observer是最简洁、性能最好的一种方案。
以 React.js 为例进行实现:
// data 为当前展示的列表数据,total为列表的总数量
// load-more 为底部“加载更多”元素的id
// 处理加载更多
useEffect(() => {
if (data.length < total && document.querySelector('#load-more')) {
const intersectionObserver = new IntersectionObserver(function (entries) {
// 如果不可见,就返回
if (entries[0].intersectionRatio <= 0) return
// 触发加载更多
triggerLoadMore?.()
})
// 开始观察
intersectionObserver.observe(document.querySelector('#load-more'))
return () => {
intersectionObserver.disconnect()
}
}
}, [data, total])
复制代码
大家可以发现,实现代码很简单,而且我们不需要再对滚动事件进行监听。
兼容性
从上表可以看出,该API的兼容性已经很不错了。
w3c也提供了对该API的polyfill,见w3c/IntersectionObserver。
日常开发中,推荐直接使用 polyfill.io 引入兼容代码:
<script src="https://polyfill.io/v3/polyfill.min.js?features=IntersectionObserver"></script>
复制代码
其他优化点
- 加入虚拟滚动
简单的理解,虚拟滚动就是在浏览器中,只渲染当前可视区域的内容,通过用户滑动滚动条的位置动态地来计算显示内容,其余部分用空白填充来给用户造成一个长列表的假象,可以解决长列表带来的页面滑动卡顿等问题。React.js 中可以使用react-window实现虚拟滚动。大家可以自行查阅资料,在此就不继续展开了。
Share
欢迎大家访问我们的 前端面试题宝典,里面已经搜集了600+常见的前端面试题的题目和答案,希望能够帮助正在面试路上的同学。
也欢迎访问我们近期更新的文章:
同时欢迎关注我们团队另一个掘金账号: