使用 RxJS 的 BehaviorSubject

Angular 也走 Redux 風 (使用 Ngrx) 一文中,我們使用了 Ngrx 這種 Redux 風格的 store 來處理 component 之間共用的 state,雖然可行,但有一點 over design,在 RxJS 出現後,我們使用 Observable Data Service 也能實現出相同的效果。

Version


macOS 10.12.4
Angular CLI 1.0.0
Angular 4.0.1

Todo 範例


service000

AppModule

app.module.ts1 1GitHub Commit : app.module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';
import {FormsModule} from '@angular/forms';
import {HttpModule} from '@angular/http';

import {AppComponent} from './app.component';
import {TodoListComponent} from './components/todo-list/todo-list.component';
import {TodoDashboardComponent} from './components/todo-dashboard/todo-dashboard.component';
import {TodoService} from './services/todo/todo.service';

@NgModule({
declarations: [
AppComponent,
TodoListComponent,
TodoDashboardComponent
],
imports: [
BrowserModule,
FormsModule,
HttpModule
],
providers: [
TodoService
],
bootstrap: [AppComponent]
})
export class AppModule { }

22 行

1
2
3
providers: [
TodoService
],

須在 AppModuleproviders 加入 TodoService

Component

AppComponent

app.component.html2 2GitHub Commit : app.component.html

1
2
3
<h1>Todo</h1>
<app-todo-list></app-todo-list>
<app-todo-dashboard></app-todo-dashboard>

包含了 TodoListTodoDashboard 兩個 component。

TodoList

todo-list.component.html3 3GitHub Commit : todo-list.component.html

1
2
3
4
5
6
7
8
<input type="text" #title>
<button (click)="addTodo(title)">Add</button>
<ul>
<li *ngFor="let todo of todos|async">
{{ todo.title }}
<button (click)="removeTodo(todo.id)">Remove</button>
</li>
</ul>

todosObservable,需加上 asyncObservable 來 subscribe 與 unsubscribe。

但這有個限制,todos 必須為宣告成 Observable<Todo[]> 型別。

Component 包含了AddRemove 2 個 button。

todo-list.component.ts4 4GitHub Commit : todo-list.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import {Component} from '@angular/core';
import {TodoService} from '../../services/todo/todo.service';
import {Todo} from '../../models/todo';
import {Observable} from 'rxjs/Observable';

@Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
todos: Observable<Todo[]>;

constructor(private todoService: TodoService) {
this.todos = this.todoService.getTodos();
}

addTodo(input: HTMLInputElement) {
if (!input.value) {
return;
}

this.todoService.addTodo(input.value);

input.value = '';
}

removeTodo(id: number) {
this.todoService.removeTodo(id);
}
}

13 行

1
2
3
constructor(private todoService: TodoService) {
this.todos = this.todoService.getTodos();
}

TodoService 依賴注入。

TodoService.getTodos() 回傳所有 todos, 此為 Observable 型別。

11 行

1
todos: Observable<Todo[]>;

宣告 todosObservable 型別,其泛型為 Todo[]

為什麼 todos 不是 Todo[] 型別,而是 Observable<Todo[]> 呢?因為 TodoService.getTodos()回傳的型別為 Observable

17 行

1
2
3
4
5
6
7
8
9
addTodo(input: HTMLInputElement) {
if (!input.value) {
return;
}

this.todoService.addTodo(input.value);

input.value = '';
}

Add button 的 event handler。

呼叫 TodoServiceaddTodo(),並直接傳入欲新增的 todo。

使用 Observable Data Service 時,就不必呼叫 Ngrxdispatch() 與傳入 action 與 payload,直接呼叫 service 的 method 與傳入資料即可。

27 行

1
2
3
removeTodo(id: number) {
this.todoService.removeTodo(id);
}

Remove button 的 event handler。

呼叫 TodoServiceremoveTodo(),並直接傳入欲移除的 id。

TodoDashboard

todo-dashboard.component.html5 5GitHub Commit : todo-dashboard.component.html

1
2
3
4
5
6
7
8
9
<p>
Last Update: {{ lastUpdate | async | date:'mediumTime'}}
</p>
<p>
Total items: {{ (todos | async ).length }}
</p>
<p>
<button (click)="clearTodos()">Delete All</button>
</p>

顯示最後更新時間與 Todo 筆數。

lastUpdatetodos 均為 Observable,需加上 asyncObservable 來 subscribe 與 unsubscribe。

Component 包含了 Clear All button。

todo-dashboard.component.ts6 6GitHub Commit : todo-dashboard.component.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {Component} from '@angular/core';
import {TodoService} from '../../services/todo/todo.service';
import {Todo} from '../../models/todo';
import {Observable} from 'rxjs/Observable';

@Component({
selector: 'app-todo-dashboard',
templateUrl: './todo-dashboard.component.html',
})
export class TodoDashboardComponent {
todos: Observable<Todo[]>;
lastUpdate: Observable<Date>;

constructor(private todoService: TodoService) {
this.todos = this.todoService.getTodos();
this.lastUpdate = this.todoService.getLastUpdate();
}

clearTodos() {
this.todoService.clearTodos();
}
}

15 行

1
2
3
4
constructor(private todoService: TodoService) {
this.todos = this.todoService.getTodos();
this.lastUpdate = this.todoService.getLastUpdate();
}

TodoService 依賴注入。

TodoService.getTodos() 回傳所有 todos, 此為 Observable 型別。

TodoService.getLastUpdate() 回傳 LastUpdate, 此為 Observable 型別。

11 行

1
2
todos: Observable<Todo[]>;
lastUpdate: Observable<Date>;

宣告 todosObservable 型別,其泛型為 Todo[]

宣告 lastUpdateObservable 型別,其泛型為 Date

19 行

1
2
3
clearTodos() {
this.todoService.clearTodos();
}

Clear All button 的 event handler。

呼叫 TodoServiceclearTodos()

Services

TodoService

todo.service.ts7 7GitHub Commit : todo.service.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import {Injectable} from '@angular/core';
import {Todo} from '../../models/todo';
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {Observable} from 'rxjs/Observable';
import {INITIAL_TODO, TodoState} from './todo-state';
import 'rxjs/add/operator/pluck';

@Injectable()
export class TodoService {
private subject = new BehaviorSubject<TodoState>(INITIAL_TODO_STATE);

getTodos(): Observable<Todo[]> {
return this.subject.pluck('todos');
}

getLastUpdate(): Observable<Date> {
return this.subject.pluck('lastUpdate');
}

addTodo(title: string) {
const {todos} = this.subject.getValue();

this.subject.next({
todos: [...todos, {
id: todos.length + 1,
title: title
}],
lastUpdate: new Date()
});
}

removeTodo(id: number) {
const {todos} = this.subject.getValue();

this.subject.next({
todos: todos.filter(todo => todo.id !== id),
lastUpdate: new Date()
});
}

clearTodos() {
this.subject.next({
todos: [],
lastUpdate: new Date()
});
}
}

全部的 state 邏輯都在 TodoService 內。

第 8 行

1
2
@Injectable()
export class TodoService {

使用 Angular 標準的 service。

10 行

1
private subject = new BehaviorSubject<TodoState>(INITIAL_TODO_STATE);

subject 為實際儲存 Observable state 之處,為 BehaviorSubject 型別,TodoState 包含 Todo[]LastUpdate

什麼是 BehaviorSubject?

根據 RxJS source code

BehaviorSubject.ts

1
export class BehaviorSubject<T> extends Subject<T> {

BehaviorSubject 繼承於 Subject

Subject.ts

1
export class Subject<T> extends Observable<T> implements ISubscription {

Subject 繼承於 Observable

也就是 Observable 所有的特性,SubjectBehaviorSubject 都有,如 subscribe() 與 RxJS 的 operator 操作。

SubjectBehaviorSubject 算是一種特殊的 Observable,提供一些原本 Observable 沒有的功能。

Observable 與 Subject 的差異?

Subject 多提供了 next(),允許我們手動將值送進 Observable

1
2
3
4
5
6
let subject = new Subject();
subject.subscribe((value) => {
console.log('Subscription got ', value);
});
subject.next('Hello World');
// Subscription got Hello World

Subject 與 BehaviorSubject 的差異?

可提供初始值給 BehaviorSubject,使用端只要 subscribe() 就能收到初始值。

1
2
3
4
5
6
7
let subject = new BehaviorSubject('Hello World');
subject.subscribe((value) => {
console.log('Subscription got ', value);
});
// Subscription got Hello World
subject.next('Hello Taiwan')
// Subscription got Hello Taiwan
  • Subject 是在 subscribe() 之後,若將來的資料有變動,會得到通知並新資料。
  • BehaviorSubject 是在 subscribe() 時,就能得到資料,因此可設定初始值。

因為希望能自己手動更新 state,且有初始值,所以我們選擇使用 BehaviorSubject,而非 ObservableSubject

12 行

1
2
3
getTodos(): Observable<Todo[]> {
return this.subject.pluck('todos');
}

回傳 todos[] 給使用端,型別為 Observable<Todo[]>

因為 Subject 是一種特殊的 Observable,所以也有 pluck operator,取出 todos 屬性值回傳。

16 行

1
2
3
getLastUpdate(): Observable<Date> {
return this.subject.pluck('lastUpdate');
}

回傳 lastUpdate 給使用端,型別為 Observable<Date>

因為 Subject 是一種特殊的 Observable,所以也有 pluck operator,取出 lastUpdate 屬性值回傳。

第 6 行

1
import 'rxjs/add/operator/pluck';

使用 pluck 時,需單獨 import 進來。

20 行

1
2
3
4
5
6
7
8
9
10
11
addTodo(title: string) {
const {todos} = this.subject.getValue();

this.subject.next({
todos: [...todos, {
id: todos.length + 1,
title: title
}],
lastUpdate: new Date()
});
}

新增 todo 進 BehaviorSubject

使用 Subject.getValue() 回傳目前 BehaviorSubject 內的 state。

使用 Subject.next() 寫入新的 state 進 BehaviorSubject

21 行

1
const {todos} = this.subject.getValue();

Subject.getValue() 會回傳 TodoState 型別

todo-state.ts

1
2
3
4
export interface TodoState {
todos: Todo[];
lastUpdate: Date;
}

內有 todoslastUpdate 兩個屬性,可使用 TypeScript 2.1 的 object destruction 將物件的屬性直接拆成兩個變數。

1
const {todos, lastUpdate} = this.subject.getValue();

因為 lastUpdate 目前用不到,只想取 todos 即可,因此省略成

1
const {todos} = this.subject.getValue();

32 行

1
2
3
4
5
6
7
8
removeTodo(id: number) {
const {todos} = this.subject.getValue();

this.subject.next({
todos: todos.filter(todo => todo.id !== id),
lastUpdate: new Date()
});
}

BehaviorSubject 移除 todo。

34 行

1
2
3
4
clearTodos() {
this.todos = [];
this.updateSubject();
}

BehaviorSubject 移除全部 todo。

TodoState

todo-state.ts8 8GitHub Commit : todo-state.ts

1
2
3
4
5
6
7
8
import {Todo} from '../../models/todo';

export interface TodoState {
todos: Todo[];
lastUpdate: Date;
}

export const INITIAL_TODO_STATE: TodoState = {todos: [], lastUpdate: null};

定義 state 型別。

第 3 行

1
2
3
4
export interface TodoState {
todos: Todo[];
lastUpdate: Date;
}

定義 TodoState 與其 field。

其中 todosTodo 型別的陣列。

第 8 行

1
export const INITIAL_TODO_STATE: TodoState = {todos: [], lastUpdate: null};

定義 INITIAL_TODO_STATE 常數,為 TodoState 的初始狀態。

Models

Todo

todo.ts9 9GitHub Commit : todo.ts

1
2
3
4
export interface Todo {
id: number;
title: string;
}

定義 Todo 型別。

Conclusion


  • 對於 component 來說,無論使用了 Ngrx/store 或 Observable Data Service,都是使用 Observable,與 state 的維護方式完全解耦合。
  • 使用了 BehaviorSubject 後,我們能手動透過 next() 維護新的 state,並能通知 component 自動更新,原本跨 component 維護 state 問題將獲得解決。

Sample Code


完整的範例可以在我的 GitHub 上找到。

Reference


Angular User Group Taiwan, 請問 Behavior Subject 與 Observable 的差異?
stackoverflow, Angular 2 - Behavior Subject vs Observable?
Angular University, How to build Angular apps using Observable Data Services - Pitfalls to avoid
Jason Watmore, Angular 2 - Communicating Between Components with Observable & Subject

2017-04-24