[译] 使用 Angular 和 RxJS 实现的无限滚动加载

原文连接: blog.strongbrew.io/infinite-sc…html

本文为 RxJS 中文社区 翻译文章,如需转载,请注明出处,谢谢合做!react

若是你也想和咱们一块儿,翻译更多优质的 RxJS 文章以奉献给你们,请点击【这里】git

关于本文

本文讲解了如何使用“响应式编程”的方式以少许的代码实现出超棒的无限滚动加载列表。对于本文,咱们将使用 RxJSAngular。若是 RxJS 对你来讲是全新概念的话,那么最好先阅读下官方文档。但不管是使用 Angular 仍是 React,都不会影响到本文的流畅度。github

响应式编程

相比于命令式编程,响应式编程有以下优点:编程

  • 再也不有“if xx, else xx” 这种场景
  • 能够忘记大量的边缘案例
  • 很容易将展示层逻辑跟其余逻辑分离 (展示层只对流做出响应)
  • 自己就是标准: 被普遍的语言所支持
  • 当理解这些概念后,能够以一种很是简单的方式将复杂的逻辑用不多的代码来实现

几天前,个人一个同事来找我探讨问题: 他想要在 Angular 中实现无限滚动加载功能,可是他无心间触碰到了命令式编程的边界。而事实也证实了,无限滚动加载解决方案其实是一个很好的例子,能够解释响应式编程如何帮助你来更好地编写代码。json

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

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

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

咱们来把无限滚动加载列表必需要知足的功能列出来:缓存

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

首先画图

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

根据上面的功能列表来看,有三个动做可使应用触发加载数据: 滚动、调整窗口大小和手动触发数据加载。当咱们用响应式思惟来思考时,能够发现有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){ 
  } 
}
复制代码

这是在线示例的地址。(译者注: 报错跑不起来。。。囧)

再一次 (正如我以前文章中所证实的),咱们不须要使用第三方解决方案来解决全部问题。无限滚动加载列表的代码并很少,并且还很是灵活。假设说咱们想减小 DOM 的压力,每次加载100条数据,那么咱们能够新建立一个流来作这件事 :)

感谢阅读本文,但愿你能喜欢。

相关文章
相关标签/搜索