无限滚动加载应该是怎样的?

无限滚动加载列表在用户将页面滚动到指定位置后会异步加载数据。这是避免寻主动加载(每次都需要用户去点击)的好方法,而且它能真正保持应用的性能。同时它还是降低带宽和增强用户体验的有效方法。

对于这种场景,假设说每个页面包含10条数据,并且所有数据都在一个可滚动的长列表中显示,这就是无限滚动加载列表。

我们来把无限滚动加载列表必须要满足的功能列出来:

  • 默认应该加载第一页的数据
  • 当首页的数据不能完全填充首屏的话,应该加载第二页的数据,以此类推,直到首屏填充满
  • 当用户向下滚动,应该加载第三页的数据,并依次类推
  • 当用户调整窗口大小后,有更多空间来展示结果,此时应该加载下一页数据
  • 应该确保同一页数据不会被加载两次 (缓存)

首先画图

就像大多数编码决策一样,先在白板上画出来是个好主意。这可能是一种个人方式,但它有助于我编写出的代码不至于在稍后阶段被删除或重构。

根据上面的功能列表来看,有三个动作可以使应用触发加载数据: 滚动、调整窗口大小和手动触发数据加载。当我们用响应式思维来思考时,可以发现有3中事件的来源,我们将其称之为流:

  • scroll 事件的流: scroll$
  • resize 事件的流: resize$
  • 手动决定加载第几页数据的流: pageByManual$

注意: 我们会给流变量加后缀$以表明这是流,这是一种约定(个人也更喜欢这种方式)

我们在白板上画出这些流:

随着时间的推移,这些流上会包含具体的值:

scroll$ 流包含 Y 值,它用来计算页码。

resize$ 流包含 event 值。我们并不需要值本身,但我们需要知道用户调整了窗口大小。

pageByManual$ 包含页码,因为它是一个 Subject,所以我们可以直接设置它。(稍后再讲)

如果我们可以将所有这些流映射成页码的流呢?那就太好了,因为基于页码才能加载指定页的数据。那么如何把当前的流映射成页码的流呢?这不是我们现在需要考虑的事情(我们只是在绘图,还记得吗?)。下一个图看起来是这样的:

从图中可以看到,我们基于初始的流创建出了下面的流:

  • pageByScroll$: 包含基于 scroll 事件的页码
  • pageByResize$: 包含基于 resize 事件的页码
  • pageByManual$: 包含基于手动事件的页码 (例如,如果页面上仍有空白区域,我们需要加载下一页数据)

如果我们能够以有效的方式合并这3个页码流,那么我们将得到一个名为 pageToLoad$ 的新的流,它包含由 scroll 事件、resize 事件和手动事件所创建的页码。

如果我们订阅 pageToLoad$ 流而不从服务中获取数据的话,那么我们的无限滚动加载已经可以部分工作了。但是,我们不是要以响应式的思维来思考吗?这就意味着要尽可能地避免订阅... 实际上,我们需要基于 pageToLoad$ 流来创建一个新的流,它将包含无限滚动加载列表中的数据...

现在将这些图合并成一个全面的设计图。

如果所示,我们有3个输入流: 它们分别负责处理滚动、调整窗口大小和手动触发。然后,我们有3个基于输入流的页码流,并将其合并成一个流,即 pageToLoad$ 流。基于 pageToLoad$ 流,我们便可以获取数据。

开始编码

图已经画的很充分了,对于无限滚动加载列表要做什么,我们也有了清晰的认知,那么我们开始编码吧。

要计算出需要加载第几页,我们需要2个属性:

private itemHeight = 40;
private numberOfItems = 10; // 页面中的项数

pageByScroll$

pageByScroll$ 流如下所示:

private pageByScroll$ =
// 首先,我们要创建一个流,它包含发生在 window 对象上的所有滚动事件
Observable.fromEvent(window, "scroll")
// 我们只对这些事件的 scrollY 值感兴趣
// 所以创建一个只包含这些值的流
.map(() => window.scrollY)
// 创建一个只包含过滤值的流
// 我们只需要当我们在视口外滚动时的值
.filter(current => current >= document.body.clientHeight - window.innerHeight)
// 只有当用户停止滚动200ms后,我们才继续执行
// 所以为这个流添加200ms的 debounce 时间
.debounceTime(200)
// 过滤掉重复的值
.distinct()
// 计算页码
.map(y => Math.ceil((y + window.innerHeight)/ (this.itemHeight * this.numberOfItems))); // --------1---2----3------2...

注意: 在真实应用中,你可能想要使用 window 和 document 的注入服务

pageByResize$

pageByResize$ 流如下所示:

  private pageByResize$ =
// 现在,我们要创建一个流,它包含发生在 window 对象上的所有 resize 事件
Observable.fromEvent(window, "resize")
// 当用户停止操作200ms后,我们才继续执行
.debounceTime(200)
// 基于 window 计算页码
.map(_ => Math.ceil(
(window.innerHeight + document.body.scrollTop) /
(this.itemHeight * this.numberOfItems)
)); // --------1---2----3------2...

pageByManual$

pageByManual$ 流用来获取初始值(首屏数据),但它同样需要我们手动控制。BehaviorSubject 非常适合,因为我们需要一个带有初始值的流,同时我们还可以手动添加值。

private pageByManual$ = new BehaviorSubject(1);

// 1---2----3------...

pageToLoad$

酷,已经有了3个页码的输入流,现在我们来创建 pageToLoad$ 流。

private pageToLoad$ =
// 将所有页码流合并成一个新的流
Observable.merge(this.pageByManual$, this.pageByScroll$, this.pageByResize$)
// 过滤掉重复的值
.distinct()
// 检查当前页码是否存在于缓存(就是组件里的一个数组属性)之中
.filter(page => this.cache[page-1] === undefined);

itemResults$

最难的部分已经完成了。现在我们拥有一个带页码的流,这十分有用。我们不再需要关心个别场景或是其他复杂的逻辑。每次 pageToLoad$ 流有新值时,我们就只加载数据即可。就这么简单!!

我们将使用 flatmap 操作符来完成,因为调用数据本身返回的也是流。FlatMap (或 MergeMap) 会将高阶 Observable 打平。

itemResults$ = this.pageToLoad$
// 基于页码流来异步加载数据
// flatMap 是 meregMap 的别名
.flatMap((page: number) => {
// 加载一些星球大战中的角色
return this.http.get(`https://swapi.co/api/people?page=${page}`)
// 创建包含这些数据的流
.map(resp => resp.json().results)
.do(resp => {
// 将页码添加到缓存中
this.cache[page -1] = resp;
// 如果页面仍有足够的空白空间,那么继续加载数据 :)
if((this.itemHeight * this.numberOfItems * page) < window.innerHeight){
this.pageByManual$.next(page + 1);
}
})
})
// 最终,只返回包含数据缓存的流
.map(_ => flatMap(this.cache));

结果

完整的代码如下所示:

注意 async pipe 负责整个订阅流程

@Component({
selector: 'infinite-scroll-list',
template: `
<table>
<tbody>
<tr *ngFor="let item of itemResults$ | async" [style.height]="itemHeight + 'px'">
<td></td>
</tr>
</tbody>
</table>
`
})
export class InfiniteScrollListComponent {
private cache = [];
private pageByManual$ = new BehaviorSubject(1);
private itemHeight = 40;
private numberOfItems = 10; private pageByScroll$ = Observable.fromEvent(window, "scroll")
.map(() => window.scrollY)
.filter(current => current >= document.body.clientHeight - window.innerHeight)
.debounceTime(200)
.distinct()
.map(y => Math.ceil((y + window.innerHeight)/ (this.itemHeight * this.numberOfItems))); private pageByResize$ = Observable.fromEvent(window, "resize")
.debounceTime(200)
.map(_ => Math.ceil(
(window.innerHeight + document.body.scrollTop) /
(this.itemHeight * this.numberOfItems)
)); private pageToLoad$ = Observable
.merge(this.pageByManual$, this.pageByScroll$, this.pageByResize$)
.distinct()
.filter(page => this.cache[page-1] === undefined); itemResults$ = this.pageToLoad$
.do(_ => this.loading = true)
.flatMap((page: number) => {
return this.http.get(`https://swapi.co/api/people?page=${page}`)
.map(resp => resp.json().results)
.do(resp => {
this.cache[page -1] = resp;
if((this.itemHeight * this.numberOfItems * page) < window.innerHeight){
this.pageByManual$.next(page + 1);
}
})
})
.map(_ => flatMap(this.cache)); constructor(private http: Http){
}
}

这是在线示例的地址

使用 Angular 和 RxJS 实现的无限滚动加载的更多相关文章

  1. Angular: 使用 RxJS Observables 来实现简易版的无限滚动加载指令

    我使用 angular-cli 来搭建项目. ng new infinite-scroller-poc --style=scss 项目生成好后,进入 infinite-scroller-poc 目录下 ...

  2. 基于 Vue.js 的移动端组件库mint-ui实现无限滚动加载更多

    通过多次爬坑,发现了这些监听滚动来加载更多的组件的共同点, 因为这些加载更多的方法是绑定在需要加载更多的内容的元素上的, 所以是进入页面则直接触发一次,当监听到滚动事件之后,继续加载更多, 所以对于无 ...

  3. PHP+InfiniteScroll实现网页无限滚动加载数据实例

    PHP+InfiniteScroll实现网页无限滚动加载数据实例,实现原理:当滚动条到底离网页底部一定长度的时候,向后台发送页数并获取数据. 首先我们在页面上先放置10条数据,即第一页,每一项都是p标 ...

  4. vue2.0无限滚动加载数据插件

      做vue项目用到下拉滚动加载数据功能,由于选的UI库(element)没有这个组件,就用Vue-infinite-loading 这个插件代替,使用中遇到的一些问题及使用方法,总结作记录! 安装: ...

  5. Vue无限滚动加载数据

    Web项目经常会用到下拉滚动加载数据的功能,今天就来种草Vue-infinite-loading 这个插件,讲解一下使用方法! 第一步:安装 npm install vue-infinite-load ...

  6. infinite-scroll插件无限滚动加载数据的使用

    网上对于infinite-scroll插件使用的例子不多.但由于它的出现,鼓吹了瀑布流形式的页面展示方式,所以不得不了解了解这种新的分页方式. 官网上有对infinite-scroll的详细描述,但一 ...

  7. AngularJS 无限滚动加载数据控件 ngInfiniteScroll

    在开发中我们可能会遇到滚动鼠标到浏览器底部实现数据的加载,js和jquery实现都不复杂都是既然AngularJS提供现成的我们怎么不用昵. ng-infinite-scroll.js这个组件则可以实 ...

  8. 【无限滚动加载数据】—infinite-scroll插件的使用

    网上对于infinite-scroll插件使用的例子不多.但由于它的出现,鼓吹了瀑布流形式的页面展示方式,所以不得不了解了解这种新的分页方式. 官网上有对infinite-scroll的详细描述,但一 ...

  9. Vue 无限滚动加载指令

    也不存在什么加载咯, 就是一个判断滚动条是否到达浏览器底部了. 如果到了就触发事件,米到就不处理. 计算公式提简单的   底部等于(0) =  滚动条高度 - 滚动条顶部距离 - 可视高度.  反正结 ...

随机推荐

  1. C# SM加密

    using System; using System.Collections.Generic; using System.Linq; using System.Text; using Org.Boun ...

  2. Lambda 表达式浅谈- 01

    已经有一段时间没有发布博文了... 今天就写一写lambda的一些简单的使用方法 Lambda 在Msdn 上的描述: Lambda 表达式是一种可用于创建委托或表达式目录树类型的匿名函数. 通过使用 ...

  3. UWP开发入门(十)——通过继承来扩展ListView

    本篇之所以起这样一个名字,是因为重点并非如何自定义控件,不涉及创建CustomControl和UserControl使用的Template和XAML概念.而是通过继承的方法来扩展一个现有的类,在继承的 ...

  4. 牛客OI赛制测试赛1 题解

    A 斐波那契 数竞生:这不是送分的常识吗? 这里引入一个叫卡西尼恒等式的玩意. 公式表达就是 设$fib[i]$为斐波那契数列的第$i$项$(i>0,i \in N_+)$ 则有 $fib[i+ ...

  5. springMVC传递一组对象的接受方式

    受益此大神:https://blog.csdn.net/cgd_8523/article/details/80022331 同时借鉴代码!!!! 我只用了一种方法,就记下这一种 需求:前台存在动态添加 ...

  6. [原创]Laravel 的缓存源码解析

    目录 前言 使用 源码 Cache Facade CacheManager Repository Store 前言 Laravel 支持多种缓存系统, 并提供了统一的api接口. (Laravel 5 ...

  7. 部署WSUS服务(一)

    引言:随着网络的发展,我们的生活也越来越离不开网络,但面临的安全威胁也越来越多.像去年爆发的针对Windows系统的勒索病毒(Wanna Cry)和年初爆发的Intel芯片漏洞告诉我们网络威胁时时刻刻 ...

  8. TX2 刷机时遇到Parsing board information failed

    因为之前调试I2C时,修改了EEPROM Layout,所以,在刷机时遇到此问题. 解决办法是按照此文档中的介绍来修改布局. 实际操作时,我拿了一块正常的TX2,按照指令: sudo i2cdump ...

  9. appium安装与部署

    前提: ①:appium属于C/S架构,代码写在Client端 ②:本章所说的部署讲的是Android设备下的Appium安装与部署 ③:Appium Client的环境是针对python3的 App ...

  10. docker 磁盘清理 相关

    用户在使用 Docker 部署业务一段时间后,可能会发现宿主节点的磁盘容量持续增长,甚至将磁盘空间耗尽进而引发宿主机异常,进而对业务造成影响. 本文先对 Docker 的空间分析与清理进行说明,然后对 ...