[RxJS] 過濾類型 Operators (4) - distinct / distinctUntilChanged / distinctUntilKeyChanged

今天來分享過濾類型的 operators - distinct 系列,這系列的 operators 都是用來避免「重複的事件資料」發生,但各有不同的用處,讓我們繼續看下去吧!

distinct

distinct 會將 Observable 內重複的值過濾掉,基本用法很簡單:

1
2
3
4
5
6
7
8
9
10
from([1, 2, 3, 3, 2, 1, 4, 5])
.pipe(distinct())
.subscribe(data => {
console.log(`distinct 示範 (1): ${data}`);
});
// distinct 示範 (1): 1
// distinct 示範 (1): 2
// distinct 示範 (1): 3
// distinct 示範 (1): 4
// distinct 示範 (1): 5

從結果可以看到,重複的事件值是不會再次發生的,例如 123 事件發生後,接著發生的 321 的事件值因為前面發生過了一樣的值了,因此被過濾掉不發生!

彈珠圖:

1
2
3
4
(1   2   3   3   2   1   4   5)
distinct()
(1 2 3 4 5)
^ 因為資料重複,不發生事件

如果今天是傳入的是物件呢?我們都知道兩個物件直接用 == 比較是不會相同的,例如以下程式碼會印出 false

1
2
3
4
const a = { id:1, score: 100 };
const b = { id:1, score: 100 };
console.log(a === b);
// false

也因此當使用 distinct operator 時,若傳入的都是物件,判斷上會有問題,這時候可以在 distinct 內加入一個 keySellector 的 callback function,callback function 會傳入每次事件的資料,並需要回傳比較用的任意值 keydistinct 會透過這個 callback function 的回傳值來決定是否重複:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const students = [
{ id: 1, score: 70 },
{ id: 2, score: 80 },
{ id: 3, score: 90 },
{ id: 1, score: 100 },
{ id: 2, score: 100 }
];
from(students)
.pipe(distinct(student => student.id))
.subscribe(student => {
console.log(`distinct 示範 (2): ${student.id} - ${student.score}`);
});
// distinct 示範 (2): 1 - 70
// distinct 示範 (2): 2 - 80
// distinct 示範 (2): 3 - 90

上面程式碼中,我們在 distinct 內加入一個 function,並回傳每個物件的 id 屬性,將這個屬性值作為資料是否重複的判斷,因此第四次事件的 id 在之前事件有發生過了,所以不會發生此事件。

distinct 內部會記錄所有發生過的事件值,我們也可以透過再多傳入一個 Observable 的方式(參數名稱為 flushes)來幫助我們判斷何時要清空紀錄事件值的內容,每當這個 Observable 有新事件發生時,就會清空來源 Observable 內用來記錄資料重複的物件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const source$ = new Subject<{id: number, score: number}>();
const sourceFlushes$ = new Subject();
source$
.pipe(distinct(student => student.id, sourceFlushes$))
.subscribe(student => {
console.log(`distinct 示範 (3): ${student.id} - ${student.score}`);
});

setTimeout(() => source$.next({ id: 1, score: 70 }), 1000);
setTimeout(() => source$.next({ id: 2, score: 80 }), 2000);
setTimeout(() => source$.next({ id: 3, score: 90 }), 3000);
setTimeout(() => source$.next({ id: 1, score: 100 }), 4000);
// 在這裡清掉 Observable distinct 內記錄資料重複的物件
setTimeout(() => sourceFlush$.next(), 4500);
setTimeout(() => source$.next({ id: 2, score: 100 }), 5000);
// distinct 示範 (3): 1 - 70
// distinct 示範 (3): 2 - 80
// distinct 示範 (3): 3 - 90
// (第四秒發生 {id: 1, score: 100},因為重複,所以事件不發生)
// (清空紀錄資料重複物件)
// distinct 示範 (3): 2 - 100 (id: 2 有發生過,但紀錄已被清空,因此事件會發生)

彈珠圖:

1
2
3
4
5
6
source$:   ---1---2---3---1---2--...
flushes$: -----------------x----...

source$.pipe(distinct(data => data), flushes$);
---1---2---3-------2--...
^ 從這裡清空紀錄,重新判斷 distinct

透過 distinct,我們可以就可以確保收到的事件資料不會重複囉。

程式碼:https://stackblitz.com/edit/mastering-rxjs-operator-distinct

distinctUntilChanged

distinctUntilChanged 會過濾掉重複的事件值,直到事件資料變更為止。

也就是說,只要目前事件資料值跟上一次事件資料值一樣,這次就事件就不會發生,若目前事件資料值跟上一次事件資料值不同時,這次事件就會發生;直接來看看程式碼:

1
2
3
4
5
6
7
8
9
from([1, 1, 2, 3, 3, 1]).pipe(
distinctUntilChanged()
).subscribe(data => {
console.log(`distinctUntilChanged 示範 (1): ${data}`)
});
// distinctUntilChanged 示範 (1): 1
// distinctUntilChanged 示範 (1): 2
// distinctUntilChanged 示範 (1): 3
// distinctUntilChanged 示範 (1): 1

第二次事件,和前一次事件一樣資料都是 1,因此該次事件不發生;第三次事件和第二次事件不同,因此第三次事件會發生。

彈珠圖:

1
2
3
4
5
(1   1    2    3    3    1)
distinctUntilChanged()
(1 2 3 1)
^ 事件值跟上次一樣,不顯示
^ 事件值跟上次不一樣,顯示

如果傳入的是物件,該怎麼比較呢?distinctUntilChanged 內可以傳入一個 compare callback function,這個 function 會傳入「目前」和「上次」的事件值,讓我們可以比較判斷是否有被變更。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const students = [
{ id: 1, score: 70 },
{ id: 1, score: 80 },
{ id: 2, score: 90 },
{ id: 3, score: 100 }
];
from(students).pipe(
distinctUntilChanged((studentA, studentB) => studentA.id === studentB.id)
)
.subscribe(student => {
console.log(`distinctUntilChanged 示範 (2): ${student.id} - ${student.score}`);
});
// distinctUntilChanged 示範 (2): 1 - 70
// distinctUntilChanged 示範 (2): 2 - 90
// distinctUntilChanged 示範 (2): 3 - 100

除此之外,distinctUntilChanged 還有第二個參數是 keySelector function,這個 function 跟 distinctkeySelector 參數一樣,是用來決定傳入的物件比較是否重複用的 key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
from(students).pipe(
distinctUntilChanged(
// compare function
(idA, idB) => idA === idB,
// keySelector function
student => student.id
)
)
.subscribe(student => {
console.log(`distinctUntilChanged 示範 (3): ${student.id} - ${student.score}`);
});
// distinctUntilChanged 示範 (3): 1 - 70
// distinctUntilChanged 示範 (3): 2 - 90
// distinctUntilChanged 示範 (3): 3 - 100

執行結果會完全一樣,但好處是我們把「決定比較的 key」和「實際比較邏輯」拆成兩個 function 了,整體閱讀上會更加容易。

程式碼:https://stackblitz.com/edit/mastering-rxjs-operator-distinctuntilchanged

distinctUntilKeyChanged

distinctUntilKeyChangeddistinctUntilChanged 基本上非常相似,但特別適合用在物件的某一個屬性就是比較用的關鍵值 (key) 的狀況,以前面 distinctUntilChanged 的例子來說,我們需要傳入比較的邏輯 (compare function),和決定物件 key 的邏輯 (keySelector function),但實際上就是比較 id 一個屬性的情況,我們就可以用 distinctUntilKeyChanged 來簡化寫法。

distinctUntilKeyChanged 的第一個參數就是事件物件的關鍵 key 值,distinctUntilKeyChanged 就會幫我們用物件的內名稱與 key 值相同的屬性,來決定資料是否重複發生。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const students = [
{ id: 1, score: 70 },
{ id: 1, score: 80 },
{ id: 2, score: 90 },
{ id: 3, score: 100 }
];
from(students).pipe(
distinctUntilKeyChanged('id')
)
.subscribe(student => {
console.log(`distinctUntilKeyChanged 示範 (1): ${student.id} - ${student.score}`);
});
// distinctUntilKeyChanged 示範 (1): 1 - 70
// distinctUntilKeyChanged 示範 (1): 2 - 90
// distinctUntilKeyChanged 示範 (1): 3 - 100

寫起來就簡單多啦!

除此之外,distinctUntilKeyChanged 還可以在再傳入一個 compare function,來決定資料是否重複:

1
2
3
4
5
6
7
8
9
10
11
12
from(students).pipe(
distinctUntilKeyChanged(
'id',
(idA, idB) => idA === idB
)
)
.subscribe(student => {
console.log(`distinctUntilKeyChanged 示範 (2): ${student.id} - ${student.score}`);
});
// distinctUntilKeyChanged 示範 (2): 1 - 70
// distinctUntilKeyChanged 示範 (2): 2 - 90
// distinctUntilKeyChanged 示範 (2): 3 - 100

當有需要自行決定忽略事件值的邏輯時,compare function 就非常好用囉!

程式碼:https://stackblitz.com/edit/mastering-rxjs-operator-distinctuntilkeychanged

本日小結

  • distinct:用來過濾「重複」的事件值發生,distinct 會把出現過的事件值記錄下來,當事件資料曾經出現過,就不讓事件發生,我們也可以自己決定何時要把這個紀錄清除。
  • distinctUntilChanged:如果事件資料「持續重複」就會被過濾掉,直到這次事件資料與上次事件資料不同時,才允許事件發生。
  • distinctUntilKeyChanged:與 distinctUntilChanged 邏輯一樣,但提供了比較簡單的方式,讓我們處理事件物件的某個屬性就是 key 值的情境。

相關資源