长列表渲染、无限下拉也算是前端开发老生常谈的问题之一了,本文将介绍一种简洁、巧妙、高效的方式来实现。话很少说,看下图,也许你能够发现什么?前端
不知你是否从上面这张图中注意到了什么,好比只是渲染了可视区域的部分 DOM ,滚动过程当中只是外层容器的 padding 在改变?git
前一点很好理解,咱们考虑到性能,不可能将一个长列表(甚至是一个无限下拉列表)的全部列表元素都进行渲染;然后一点,则是本文所介绍方案的核心之一!github
不卖关子,提早告诉你该方案的要素就是两个:web
说明了要素,也许你能够尝试着开始思考,看你是否能猜到具体的实现方案。npm
一直以来,检测元素的可视状态或者两个元素的相对可视状态都不是件容易事。传统的各类方案不但复杂,并且性能成本很高,好比须要监听滚动事件,而后查询 DOM , 获取元素高度、位置,计算距离视窗高度等等。数组
这就是 Intersection Observer 要解决的问题。它为开发人员提供一种便捷的新方法来异步查询元素相对于其余元素或视窗的位置,消除了昂贵的 DOM 查询和样式读取成本。缓存
主要在 Safari 上兼容性较差,须要 12.2 及以上才兼容,不过还好,有 polyfill 可食用。dom
基本了解 Intersection Observer 以后,接下来就看下如何用 Intersection Observer + padding 来实现无限下拉。异步
先概览下整体思路:函数
核心:利用父元素的 padding 去填充随着无限下拉而本该有的、愈来愈多的 DOM 元素,仅仅保留视窗区域上下必定数量的 DOM 元素来进行数据渲染。
// 观察者建立
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) {
// 当最后一个元素进入视窗
}
});
};
复制代码
拿具体例子来讲明,咱们用一个数组来维护须要渲染到页面中的数据。数组的长度会随着不断请求新的数据而不断变大,而渲染的始终是其中必定数量的元素,好比 20 个。 那么:
一、最开始渲染的是数组中序号为 0 - 19 的元素,即此时对应的 firstIndex 为 0;
二、当序号为 19 的元素(即上一步的 lastItem )进入视窗时,咱们就会日后渲染 10 个元素,即渲染序号为 10 - 29 的元素,那么此时的 firstIndex 为 10;
三、下一次就是,当序号为 29 的元素进入视窗时,继续日后渲染 10个元素,即渲染序号为 20 - 39 的元素,那么此时的 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,是为了根据页面的滚动状况,知道接下来哪些数据应该被获取、渲染。
const renderFunction = (firstIndex) => {
// offset = firstIndex, limit = 10 => getData
// getData Done => new dataItems => render DOM
};
复制代码
这一部分就是根据 firstIndex 查询数据,而后将目标数据渲染到页面上便可。
既然数据的更新以及 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 的实现概要:
iScroll 经过对传统滚动事件的监听,获取滚动距离,而后:
相关对比:
本文发布自 网易云音乐前端团队,欢迎自由转载,转载请保留出处。咱们一直在招人,若是你刚好准备换工做,又刚好喜欢云音乐,那就 加入咱们