rx用expand達成recursive

昨天看了一部影片,是由大神Ben介紹expand的功能

這篇是跟著實作並且去理解使用,將透過pagination的應用來做介紹,每頁10筆,假設不確定會有幾頁,要全部都拿回來的話可以怎麼實作

模擬API資料

首先我們要先有模擬api的功能,因此我們寫一段rx並帶上500的timer作為示意,這邊就固定只給到10頁

1
2
3
4
5
6
7
8
9
10
11
12
13
const getPagedData = index => timer(500).pipe(
map(()=>({
pageIndex: index,
data: Array.from({ length: 10 }, (_, i)=>({
id: (index * 10) + i,
data: Math.random(),
})),
nextPageIndex: index < 10 ? index + 1: undefined,
})),
);

const source = getPagedData(0);
source.subscribe(p=>console.log(p));

執行後可以看到這樣的資料,那初期的準備就完成了

1
2
3
4
5
{
data: Array[10],
nextPageIndex: 1,
pageIndex: 0
}

recursive實作

如果用原本熟悉的那些operator下去做,會怎麼實作呢?這時候我會使用subject來達到這個目的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const nextPage$ = new Subject();

nextPage$.pipe(
concatMap(index => getPagedData(index))
).subscribe(p=>{
const {nextPageIndex} = p;
if(nextPageIndex !== undefined){
nextPage$.next(nextpageIndex);
}

console.log(p);
});

nextPage$.next(0);

其實subscribe的那段判斷,可以拉上去變成用tap來做

1
2
3
4
const source = nextPage$.pipe(
concatMap(index => getPagedData(index)),
tap({nextPageIndex} => nextPageIndex !== undefined && nextPage$.next(nextpageIndex))
);

重構改用expand

但rx有提供更好的寫法,就是expand先來看看官方的說法

Recursively projects each source value to an Observable which is merged in the output Observable.

這非常類似於mergeMap但在條件完成前就是不斷的呼叫同一個方法,直到呼叫EMPTY為止,那就來試著改寫上面的那段程式

1
expand({nextPageIndex} => nextPageIndex !== undefined ? getPagedData(nextPageIndex) : EMPTY)

哇賽,整個變得好簡潔呢,但是來源資料要怎麼給呢?
因為我們改成用expand,因此資料要符合他的格式,才能讓他遞迴下去,因此就改成用這樣的寫法

1
2
3
const source = of({nextPageIndex: 0}).pipe(
expand({nextPageIndex} => nextPageIndex !== undefined ? getPagedData(nextPageIndex) : EMPTY)
);

結論

雖然這個應用場景我還沒遇到,但能多學到一個operator是還蠻不錯的,畢竟rx所提供的operator還蠻多的,而且只看範例完全無法想像那個應用情境,剛好有大神開示,試著自己玩玩看還不錯!

完整的程式碼可以到這邊來看!

參考

https://rxjs.dev/api/operators/expand