[譯] 使用 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條數據,那麼咱們能夠新建立一個流來作這件事 :)

感謝閱讀本文,但願你能喜歡。

相關文章
相關標籤/搜索