本文主旨
长列表渲染、无限下拉也算是前端开发老生常谈的问题之一了,本文将介绍一种简洁、巧妙、高效的方式来实现。话不多说,看下图,也许你可以发现什么?
不知你是否从上面这张图中注意到了什么,比如只是渲染了可视区域的部分 DOM ,滚动过程中只是外层容器的 padding 在改变?
前一点很好理解,我们考虑到性能,不可能将一个长列表(甚至是一个无限下拉列表)的所有列表元素都进行渲染;而后一点,则是本文所介绍方案的核心之一!
不卖关子,提前告诉你该方案的要素就是两个:
说明了要素,也许你可以尝试着开始思考,看你是否能猜到具体的实现方案。
方案介绍
Intersection Observer
基本概念
一直以来,检测元素的可视状态或者两个元素的相对可视状态都不是件容易事。传统的各种方案不但复杂,而且性能成本很高,比如需要监听滚动事件,然后查询 DOM , 获取元素高度、位置,计算距离视窗高度等等。
这就是 Intersection Observer 要解决的问题。它为开发人员提供一种便捷的新方法来异步查询元素相对于其他元素或视窗的位置,消除了昂贵的 DOM 查询和样式读取成本。
兼容性
主要在 Safari 上兼容性较差,需要 12.2 及以上才兼容,不过还好,有 polyfill 可食用。
一些应用场景
padding 方案实现
基本了解 Intersection Observer 之后,接下来就看下如何用 Intersection Observer + padding 来实现无限下拉。
先概览下总体思路:
核心:利用父元素的 padding 去填充随着无限下拉而本该有的、越来越多的 DOM 元素,仅仅保留视窗区域上下一定数量的 DOM 元素来进行数据渲染。
1、监听一个固定长度列表的首尾元素是否进入视窗
// 观察者创建
this.observer = new IntersectionObserver(callback, options);
// 观察列表第一个以及最后一个元素
this.observer.observe(this.firstItem);
this.observer.observe(this.lastItem);
我们以在页面中渲染固定的 20 个列表元素为例,我们对第一个元素和最后一个元素,用 Intersection Observer 进行观察,当他们其中一个重新进入视窗时,callback 函数就会触发:
const callback = (entries) => {
entries.forEach((entry) => {
if (entry.target.id === firstItemId) {
// 当第一个元素进入视窗
} else if (entry.target.id === lastItemId) {
// 当最后一个元素进入视窗
}
});
};
2、更新当前页面渲染的第一个元素对应的序号 (firstIndex)
拿具体例子来说明,我们用一个数组来维护需要渲染到页面中的数据。数组的长度会随着不断请求新的数据而不断变大,而渲染的始终是其中一定数量的元素,比如 20 个。 那么:
// 我们对原先的 firstIndex 做了缓存
const { currentIndex } = this.domDataCache;
// 以全部容器内所有元素的一半作为每一次渲染的增量
const increment = Math.floor(this.listSize / 2);
let firstIndex;
if (isScrollDown) {
// 向下滚动时序号增加
firstIndex = currentIndex + increment;
} else {
// 向上滚动时序号减少
firstIndex = currentIndex - increment;
}
总体来说,更新 firstIndex,是为了根据页面的滚动情况,知道接下来哪些数据应该被获取、渲染。
3、根据上述序号,获取对应数据元素,列表重新渲染成新的内容
const renderFunction = (firstIndex) => {
// offset = firstIndex, limit = 10 => getData
// getData Done => new dataItems => render DOM
};
这一部分就是根据 firstIndex 查询数据,然后将目标数据渲染到页面上即可。
4、padding 调整,模拟滚动实现
既然数据的更新以及 DOM 元素的更新我们已经实现了,那么无限下拉的效果以及滚动的体验,我们要如何实现呢?
想象一下,抛开一切,最原始最直接最粗暴的方式无非就是我们再又获取了 10 个新的数据元素之后,再塞 10 个新的 DOM 元素到页面中去来渲染这些数据。
但此时,对比上面这个粗暴的方案,我们的方案是:这 10个新的数据元素,我们用原来已有的 DOM 元素去渲染,替换掉已经离开视窗、不可见的数据元素;而本该由更多 DOM 元素进一步撑开容器高度的部分,我们用 padding 填充来模拟实现。
">
// padding的增量 = 每一个item的高度 x 新的数据项的数目
const remPaddingsVal = itemHeight * (Math.floor(this.listSize / 2));
if (isScrollDown) {
// paddingTop新增,填充顶部位置
newCurrentPaddingTop = currentPaddingTop + remPaddingsVal;
if (currentPaddingBottom === 0) {
newCurrentPaddingBottom = 0;
} else {
// 如果原来有paddingBottom则减去,会有滚动到底部的元素进行替代
newCurrentPaddingBottom = currentPaddingBottom - remPaddingsVal;
}
}
// padding的增量 = 每一个item的高度 x 新的数据项的数目
const remPaddingsVal = itemHeight * (Math.floor(this.listSize / 2));
if (!isScrollDown) {
// paddingBottom新增,填充底部位置
newCurrentPaddingBottom = currentPaddingBottom + remPaddingsVal;
if (currentPaddingTop === 0) {
newCurrentPaddingTop = 0;
} else {
// 如果原来有paddingTop则减去,会有滚动到顶部的元素进行替代
newCurrentPaddingTop = currentPaddingTop - remPaddingsVal;
}
}
// 容器padding重新设置
this.updateContainerPadding({
newCurrentPaddingBottom,
newCurrentPaddingTop
})
// DOM元素相关数据缓存更新
this.updateDomDataCache({
currentPaddingTop: newCurrentPaddingTop,
currentPaddingBottom: newCurrentPaddingBottom
});
思考总结
方案总结:
利用 Intersection Observer 来监测相关元素的滚动位置,异步监听,尽可能得减少 DOM 操作,触发回调,然后去获取新的数据来更新页面元素,并且用调整容器 padding 来替代了本该越来越多的 DOM 元素,最终实现列表滚动、无限下拉。
相关方案的对比
这里和较为有名的库 - iScroll 实现的无限下拉方案进行一个基本的对比,对比之前先说明下 iScroll infinite 的实现概要:
存在的缺陷:
延伸拓展
代码实现
参考文章
原文作者:云音乐前端技术团队
原文链接:https://juejin.im/post/5de5baf2518825235b095cbe
来源:掘金