- 简洁、有趣又高效的无限下拉方案
- 来源:小白冲冲冲冲
列表渲染、无限下拉也算是前端开发老生常谈的问题之一了,本文将介绍一种简洁、巧妙、高效的方式来实现。话不多说,看下图,也许你可以发现什么?
不知你是否从上面这张图中注意到了什么,比如只是渲染了可视区域的部分 DOM ,滚动过程中只是外层容器的 padding 在改变?前一点很好理解,我们考虑到性能,不可能将一个长列表(甚至是一个无限下拉列表)的所有列表元素都进行渲染;而后一点,则是本文所介绍方案的核心之一!不卖关子,提前告诉你该方案的要素就是两个:Intersection Observerpadding说明了要素,也许你可以尝试着开始思考,看你是否能猜到具体的实现方案。方案介绍Intersection Observer基本概念一直以来,检测元素的可视状态或者两个元素的相对可视状态都不是件容易事。传统的各种方案不但复杂,而且性能成本很高,比如需要监听滚动事件,然后查询 DOM , 获取元素高度、位置,计算距离视窗高度等等。这就是 Intersection Observer 要解决的问题。它为开发人员提供一种便捷的新方法来异步查询元素相对于其他元素或视窗的位置,消除了昂贵的 DOM 查询和样式读取成本。兼容性主要在 Safari 上兼容性较差,需要 12.2 及以上才兼容,不过还好,有 polyfill 可食用。一些应用场景页面滚动时的懒加载实现。无限下拉(本文的实现)。监测某些广告元素的曝光情况来做相关数据统计。监测用户的滚动行为是否到达了目标位置来实现一些交互逻辑(比如视频元素滚动到隐藏位置时暂停播放)。我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取padding 方案实现基本了解 Intersection Observer 之后,接下来就看下如何用 Intersection Observer + padding 来实现无限下拉。先概览下总体思路:监听一个固定长度列表的首尾元素是否进入视窗;更新当前页面内渲染的第一个元素对应的序号;根据上述序号,获取目标数据元素,列表内容重新渲染成对应内容;容器 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 个。那么:1、最开始渲染的是数组中序号为 0 - 19 的元素,即此时对应的 firstIndex 为 0;2、当序号为 19 的元素(即上一步的 lastItem )进入视窗时,我们就会往后渲染 10 个元素,即渲染序号为 10 - 29 的元素,那么此时的 firstIndex 为 10;3、下一次就是,当序号为 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,是为了根据页面的滚动情况,知道接下来哪些数据应该被获取、渲染。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 填充来模拟实现。
向下滚动const
renderFunction
=
(firstIndex) =>
{
// offset = firstIndex, limit = 10 => getData
// getData Done => new dataItems => render DOM
};
向上滚动// 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 设置更新以及相关缓存数据更新// 容器padding重新设置
this
.
updateContainerPadding
({
newCurrentPaddingBottom
,
newCurrentPaddingTop
})
// DOM元素相关数据缓存更新
this
.
updateDomDataCache
({
currentPaddingTop
:
newCurrentPaddingTop
,
currentPaddingBottom
:
newCurrentPaddingBottom
});思考总结方案总结:利用 Intersection Observer 来监测相关元素的滚动位置,异步监听,尽可能得减少 DOM 操作,触发回调,然后去获取新的数据来更新页面元素,并且用调整容器 padding 来替代了本该越来越多的 DOM 元素,最终实现列表滚动、无限下拉。相关方案的对比这里和较为有名的库 - iScroll 实现的无限下拉方案进行一个基本的对比,对比之前先说明下 iScroll infinite 的实现概要:
iScroll 通过对传统滚动事件的监听,获取滚动距离,然后:设置父元素的 translate 来实现整体内容的上移(下移);再基于这个滚动距离进行相应计算,得知相应子元素已经被滚动到视窗外,并且判断是否应该将这些离开视窗的子元素移动到末尾,从而再对它们进行 translate 的设置来移动到末尾。这就像是一个循环队列一样,随着滚动的进行,顶部元素先出视窗,但又将移动到末尾,从而实现无限下拉。
相关对比:实现对比:一个是 Intersection Observer 的监听,来通知子元素离开视窗,只要定量设置父元素 padding 就行;另一个是对传统滚动事件的监听,滚动距离的获取,再进行一系列计算,去设置父元素以及子元素的 translate。显而易见,前者看起来更加简洁明了一些。性能对比:我知道说到对比,你脑海中肯定一下子会想到性能问题。其实性能对比的关键就是 Intersection Observer。因为单就 padding 设置还是 translate 设置,性能方面的差距是甚小的,只是个人感觉 padding 会简洁些?而 Intersection Observer 其实抽离了所有滚动层面的相关逻辑,你不再需要对滚动距离等相应 DOM 属性进行获取,也不再需要进行一系列滚动距离相关的复杂计算,并且同步的滚动事件触发变成异步的,你也不再需要另外去做防抖之类的逻辑,这在性能方面还是有所提升的。存在的缺陷:padding 的计算依赖列表项固定的高度。这是一个同步渲染的方案,也就是目前容器 padding 的计算调整,无法计算异步获取的数据,只跟用户的滚动行为有关。这看起来与实际业务场景有些不符。解决思路:思路 1、利用 Skeleton Screen Loading 来同步渲染数据元素,不受数据异步获取的影响。即在数据请求还未完成时,先使用一些图片进行占位,待内容加载完成之后再进行替换。思路 2、滚动到目标位置,阻塞容器 padding 的设置(即无限下拉的发生)直至数据请求完毕,用 loading gif 提示用户加载状态,但这个方案相对复杂,你需要全面考虑用户难以预测的滚动行为来设置容器的 padding。 原文链接:https://mp.weixin.qq.com/s/xEE7ZyETb3CkP5Y4RWuCeA
作者:小生方勤
不知你是否从上面这张图中注意到了什么,比如只是渲染了可视区域的部分 DOM ,滚动过程中只是外层容器的 padding 在改变?前一点很好理解,我们考虑到性能,不可能将一个长列表(甚至是一个无限下拉列表)的所有列表元素都进行渲染;而后一点,则是本文所介绍方案的核心之一!不卖关子,提前告诉你该方案的要素就是两个:Intersection Observerpadding说明了要素,也许你可以尝试着开始思考,看你是否能猜到具体的实现方案。方案介绍Intersection Observer基本概念一直以来,检测元素的可视状态或者两个元素的相对可视状态都不是件容易事。传统的各种方案不但复杂,而且性能成本很高,比如需要监听滚动事件,然后查询 DOM , 获取元素高度、位置,计算距离视窗高度等等。这就是 Intersection Observer 要解决的问题。它为开发人员提供一种便捷的新方法来异步查询元素相对于其他元素或视窗的位置,消除了昂贵的 DOM 查询和样式读取成本。兼容性主要在 Safari 上兼容性较差,需要 12.2 及以上才兼容,不过还好,有 polyfill 可食用。一些应用场景页面滚动时的懒加载实现。无限下拉(本文的实现)。监测某些广告元素的曝光情况来做相关数据统计。监测用户的滚动行为是否到达了目标位置来实现一些交互逻辑(比如视频元素滚动到隐藏位置时暂停播放)。我自己是一名从事了多年开发的web前端老程序员,目前辞职在做自己的web前端私人定制课程,今年年初我花了一个月整理了一份最适合2019年学习的web前端学习干货,各种框架都有整理,送给每一位前端小伙伴,想要获取的可以关注我的头条号并在后台私信我:前端,即可免费获取padding 方案实现基本了解 Intersection Observer 之后,接下来就看下如何用 Intersection Observer + padding 来实现无限下拉。先概览下总体思路:监听一个固定长度列表的首尾元素是否进入视窗;更新当前页面内渲染的第一个元素对应的序号;根据上述序号,获取目标数据元素,列表内容重新渲染成对应内容;容器 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 个。那么:1、最开始渲染的是数组中序号为 0 - 19 的元素,即此时对应的 firstIndex 为 0;2、当序号为 19 的元素(即上一步的 lastItem )进入视窗时,我们就会往后渲染 10 个元素,即渲染序号为 10 - 29 的元素,那么此时的 firstIndex 为 10;3、下一次就是,当序号为 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,是为了根据页面的滚动情况,知道接下来哪些数据应该被获取、渲染。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 填充来模拟实现。
向下滚动const
renderFunction
=
(firstIndex) =>
{
// offset = firstIndex, limit = 10 => getData
// getData Done => new dataItems => render DOM
};
向上滚动// 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 设置更新以及相关缓存数据更新// 容器padding重新设置
this
.
updateContainerPadding
({
newCurrentPaddingBottom
,
newCurrentPaddingTop
})
// DOM元素相关数据缓存更新
this
.
updateDomDataCache
({
currentPaddingTop
:
newCurrentPaddingTop
,
currentPaddingBottom
:
newCurrentPaddingBottom
});思考总结方案总结:利用 Intersection Observer 来监测相关元素的滚动位置,异步监听,尽可能得减少 DOM 操作,触发回调,然后去获取新的数据来更新页面元素,并且用调整容器 padding 来替代了本该越来越多的 DOM 元素,最终实现列表滚动、无限下拉。相关方案的对比这里和较为有名的库 - iScroll 实现的无限下拉方案进行一个基本的对比,对比之前先说明下 iScroll infinite 的实现概要:
iScroll 通过对传统滚动事件的监听,获取滚动距离,然后:设置父元素的 translate 来实现整体内容的上移(下移);再基于这个滚动距离进行相应计算,得知相应子元素已经被滚动到视窗外,并且判断是否应该将这些离开视窗的子元素移动到末尾,从而再对它们进行 translate 的设置来移动到末尾。这就像是一个循环队列一样,随着滚动的进行,顶部元素先出视窗,但又将移动到末尾,从而实现无限下拉。
相关对比:实现对比:一个是 Intersection Observer 的监听,来通知子元素离开视窗,只要定量设置父元素 padding 就行;另一个是对传统滚动事件的监听,滚动距离的获取,再进行一系列计算,去设置父元素以及子元素的 translate。显而易见,前者看起来更加简洁明了一些。性能对比:我知道说到对比,你脑海中肯定一下子会想到性能问题。其实性能对比的关键就是 Intersection Observer。因为单就 padding 设置还是 translate 设置,性能方面的差距是甚小的,只是个人感觉 padding 会简洁些?而 Intersection Observer 其实抽离了所有滚动层面的相关逻辑,你不再需要对滚动距离等相应 DOM 属性进行获取,也不再需要进行一系列滚动距离相关的复杂计算,并且同步的滚动事件触发变成异步的,你也不再需要另外去做防抖之类的逻辑,这在性能方面还是有所提升的。存在的缺陷:padding 的计算依赖列表项固定的高度。这是一个同步渲染的方案,也就是目前容器 padding 的计算调整,无法计算异步获取的数据,只跟用户的滚动行为有关。这看起来与实际业务场景有些不符。解决思路:思路 1、利用 Skeleton Screen Loading 来同步渲染数据元素,不受数据异步获取的影响。即在数据请求还未完成时,先使用一些图片进行占位,待内容加载完成之后再进行替换。思路 2、滚动到目标位置,阻塞容器 padding 的设置(即无限下拉的发生)直至数据请求完毕,用 loading gif 提示用户加载状态,但这个方案相对复杂,你需要全面考虑用户难以预测的滚动行为来设置容器的 padding。 原文链接:https://mp.weixin.qq.com/s/xEE7ZyETb3CkP5Y4RWuCeA
作者:小生方勤
-
-
- 网事沸点:2020年元旦火车票开售;多地上调最低工资标准
- 政经事、IT圈、身边事、世界眼……今日哪些网事最热?网络传播杂志联合微热点大数据研究院,为你带来每日最热的“网事早餐”。 政 经 事 工信部对《新能源汽车产业发展规划(2021-2035年)》公开征求
- 微热点
-
-
-
- “奶茶妹”章泽天被知名导演微博怒怼:你算什么成功人士?
- 奶茶妹章泽天是大家都非常熟悉的一个人,虽然她也不完全属于是娱乐圈里的人,但娱乐圈里总是少不了她的传说。当年凭着一张图片爆红网络,到后来和京东掌门人刘强东结婚,更是赚足了网友的眼球。 要是说章泽天为什么
- 众乐众开心
-
-
-
- 简洁、有趣又高效的无限下拉方案
- 列表渲染、无限下拉也算是前端开发老生常谈的问题之一了,本文将介绍一种简洁、巧妙、高效的方式来实现。话不多说,看下图,也许你可以发现什么? 不知你是否从上面这张图中注意到了什么,比如只是渲染了可视区
- 小白冲冲冲冲
-
-
-
- 突发:皮柴升任Alphabet CEO!佩奇布林卸任,权力交接完成
- 美国时间周二12月3日下午,拉里·佩奇 (Larry Page) 和谢尔盖·布林 (Sergey Brin) 突然宣布,从即日起辞任 Alphabet CEO 和总裁。 Google 现任 CEO 颂
- PingWest品玩
-
-
-
- 价值7980元的411集Python教程,在此泄露给大家,速度来拿
- 我是完全自学的,看完这条回答你就不用花钱学了。一千多的课程也就是基础课,并不会给你有多深入。如果要是学基础的话,看看下面的学习线路图以及相关的学习方法足够了。贴一个我之前自学的路径图和方法。 小白学P
- 大叔教你学编程
-
-
-
- 中国移动提醒:手机上这些照片尽量删除,放在手机上不安全
- 智能手机的出现可以说的方便了大家的生活,也丰富了大家的视野,不管出现什么事情都会出现在网上,然后大家就可以得知,这种信息传递方式也是可以说是洞察世界大事,凡是有利就有弊,智能手机的出现也暴露出很多问题
- 吖薛Vlog
-
-
-
- 26岁女孩捐款24000万元,亮眼“中国慈善榜”完爆大部分明星
- 18岁考上清华大学,22岁嫁入豪门成为刘强东的老婆,2017年位列新财富500富人榜第29位,成为榜单中上榜的最年轻富豪。如今的章泽天的主要工作是投资以及做慈善,2017年6月2日,刘强东与章泽天向中
- 燚燊焱炎火
-
-
-
- 苹果又开始炫技了!新iPad Pro或将采用mini-LED显示屏
- 年关将至,离苹果发布新款 iPad Pro 的日子也越来越近了。在最近的投资者说明当中,长期的苹果分析师郭老师表示,他表示苹果将会在明年的 iPad Pro 当中采用 mini-LED 屏幕,之后这种
- 安卓中国
-
-
-
- 两年时间,微信小程序是怎么做到日活就达到了3.5亿
- 在IPhone诞生的十周年,微信的张小龙为了向乔布斯致敬,选在了2017年1月9号推出了“微信小程序”,从17年到19年,两年时间过去了,这对于一款互联网产品而言,已到了“而立”之年,从人人唱衰到人人
- 技术小助手
-
-
-
- 特斯拉获得15辆Cybertruck的订单,可改装成为警车
- 特斯拉Cybertruck吸引了许多人的注意力,其中包括墨西哥市长,他决定从特斯拉订购15辆电动卡车,将其转换成警车。当特斯拉上个月发布Cybertruck时,该设计震惊了很多人,但其他人则专注于规格
- 云东方
-
-
-
- 运营商如何找到5G需求的新商业模式
- 5G网络技术由于其卓越的通信能力,被多国政府视为带动通信产业及相关垂直行业发展的“利器”。5G网络的峰值速率较4G提升10倍以上,移动性提升1.5倍,时延降至1/10,最低可到1ms级,每平方公里连接
- 通信世界
-
-
-
- 小米9正式从官网下架,性价比手机时代要结束了?
- 还记得当年小米手机以1999元起售的旗舰手机,成功创造了属于中国手机的性价比元年。对于米粉,虽然预约抢购会是煎熬的过程,但绝对也是珍贵的回忆。毕竟,当你提前入手1999元高配置手机的时候,一种莫名的兴
- 科技漫阅媒
-
-
-
- 中天科技光缆家族再添新成员 实现多项技术创新
- 日前,中天科技三款光缆新产品、新技术通过鉴定,综合性能达到国际先进水平。 这三款新产品、新技术分别是“陆地用通信金属松套管光缆”、“微束管气吹微缆”和“基于RFID的光缆识别技术”。鉴定会上,与会专家
- 同花顺财经
-
-
-
- 特斯拉新专利:利用激光束自动清除汽车挡风玻璃上的碎片
- 除了苹果是一个专利狂魔之外,特斯拉也是毫不逊色,并且又申请了一项新专利,想要用激光束自动清除汽车玻璃制品上的碎片。 盖世汽车讯 据外媒报道,特斯拉(Tesla)在研发一项新技术,该技术由安装在车上的激
- 盖世汽车
-
-
-
- 13万个基站:5G“飞”入寻常百姓家
- 【2019,数说变化】 【数说】13万个基站,一个充满期待的数字——在日前举办的2019世界5G大会上,工业和信息化部部长苗圩介绍,目前全国已经开通5G基站11.3万个,预计到年底将达13万个,5G套
- 光明日报
-