原文連接: blog.strongbrew.io/infinite-sc…html
本文爲 RxJS 中文社區 翻譯文章,如需轉載,請註明出處,謝謝合做!react
若是你也想和咱們一塊兒,翻譯更多優質的 RxJS 文章以奉獻給你們,請點擊【這裏】git
本文講解了如何使用「響應式編程」的方式以少許的代碼實現出超棒的無限滾動加載列表。對於本文,咱們將使用 RxJS 和 Angular。若是 RxJS 對你來講是全新概念的話,那麼最好先閱讀下官方文檔。但不管是使用 Angular 仍是 React,都不會影響到本文的流暢度。github
相比於命令式編程,響應式編程有以下優點:編程
幾天前,個人一個同事來找我探討問題: 他想要在 Angular 中實現無限滾動加載功能,可是他無心間觸碰到了命令式編程的邊界。而事實也證實了,無限滾動加載解決方案其實是一個很好的例子,能夠解釋響應式編程如何幫助你來更好地編寫代碼。json
無限滾動加載列表在用戶將頁面滾動到指定位置後會異步加載數據。這是避免尋主動加載(每次都須要用戶去點擊)的好方法,並且它能真正保持應用的性能。同時它仍是下降帶寬和加強用戶體驗的有效方法。api
對於這種場景,假設說每一個頁面包含10條數據,而且全部數據都在一個可滾動的長列表中顯示,這就是無限滾動加載列表。數組
咱們來把無限滾動加載列表必需要知足的功能列出來:緩存
就像大多數編碼決策同樣,先在白板上畫出來是個好主意。這多是一種我的方式,但它有助於我編寫出的代碼不至於在稍後階段被刪除或重構。angular2
根據上面的功能列表來看,有三個動做可使應用觸發加載數據: 滾動、調整窗口大小和手動觸發數據加載。當咱們用響應式思惟來思考時,能夠發現有3中事件的來源,咱們將其稱之爲流:
注意: 咱們會給流變量加後綴$以代表這是流,這是一種約定(我的也更喜歡這種方式)
咱們在白板上畫出這些流:
隨着時間的推移,這些流上會包含具體的值:
scroll$
流包含 Y 值,它用來計算頁碼。
resize$
流包含 event 值。咱們並不須要值自己,但咱們須要知道用戶調整了窗口大小。
pageByManual$
包含頁碼,由於它是一個 Subject,因此咱們能夠直接設置它。(稍後再講)
若是咱們能夠將全部這些流映射成頁碼的流呢?那就太好了,由於基於頁碼才能加載指定頁的數據。那麼如何把當前的流映射成頁碼的流呢?這不是咱們如今須要考慮的事情(咱們只是在繪圖,還記得嗎?)。下一個圖看起來是這樣的:
從圖中能夠看到,咱們基於初始的流建立出了下面的流:
若是咱們可以以有效的方式合併這3個頁碼流,那麼咱們將獲得一個名爲 pageToLoad$
的新的流,它包含由 scroll 事件、resize 事件和手動事件所建立的頁碼。
若是咱們訂閱 pageToLoad$
流而不從服務中獲取數據的話,那麼咱們的無限滾動加載已經能夠部分工做了。可是,咱們不是要以響應式的思惟來思考嗎?這就意味着要儘量地避免訂閱... 實際上,咱們須要基於 pageToLoad$
流來建立一個新的流,它將包含無限滾動加載列表中的數據...
如今將這些圖合併成一個全面的設計圖。
若是所示,咱們有3個輸入流: 它們分別負責處理滾動、調整窗口大小和手動觸發。而後,咱們有3個基於輸入流的頁碼流,並將其合併成一個流,即 pageToLoad$
流。基於 pageToLoad$
流,咱們即可以獲取數據。
圖已經畫的很充分了,對於無限滾動加載列表要作什麼,咱們也有了清晰的認知,那麼咱們開始編碼吧。
要計算出須要加載第幾頁,咱們須要2個屬性:
private itemHeight = 40;
private numberOfItems = 10; // 頁面中的項數
複製代碼
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$
流以下所示:
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$
流用來獲取初始值(首屏數據),但它一樣須要咱們手動控制。BehaviorSubject
很是適合,由於咱們須要一個帶有初始值的流,同時咱們還能夠手動添加值。
private pageByManual$ = new BehaviorSubject(1);
// 1---2----3------...
複製代碼
酷,已經有了3個頁碼的輸入流,如今咱們來建立 pageToLoad$
流。
private pageToLoad$ =
// 將全部頁碼流合併成一個新的流
Observable.merge(this.pageByManual$, this.pageByScroll$, this.pageByResize$)
// 過濾掉重複的值
.distinct()
// 檢查當前頁碼是否存在於緩存(就是組件裏的一個數組屬性)之中
.filter(page => this.cache[page-1] === undefined);
複製代碼
最難的部分已經完成了。如今咱們擁有一個帶頁碼的流,這十分有用。咱們再也不須要關心個別場景或是其餘複雜的邏輯。每次 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條數據,那麼咱們能夠新建立一個流來作這件事 :)
感謝閱讀本文,但願你能喜歡。