如果對於 Observable 的 .reduce .scan
不熟的話,建議回頭看一下[ngrx/store -3] Observable 的 運算子 (Operator)。基本上.scan()
會一直持續做,而 reduce()
會等到 complete
時才一次將結果產生,而我們要的 Reducer 其實是一直循環持續做而且能用到前面的狀態,用 .scan()
來做 Reducer 是最適當的 Operator。
先從 Reducer 的輸出跟輸入看起
記得我們在 [ngrx/store-7] 純函數 (Pure Function) 以及 [ngrx/store-8] Javascript Mutable 跟 Immutable 資料型態談過純函數,如果您還沒看過這兩篇文章,建議您花些時間看一下,我們來寫一個最簡單的 Reducer
// reducer
const message = (state = [], action) => {
switch (action.type) {
case 'ADD_MESSAGE':
return [...state, action.payload];
case 'REMOVE_MESSAGE':{
return state.filter(msg => msg.id != action.payload);
}
default:
return state;
}
}
這裡我們的狀態樹是一個物件的陣列,當增加一筆物件到陣列時(ADD_MESSAGE),我們使用擴展語法 ...
拷貝原來陣列,再將新的物件放入陣列中,可以放在前面也可以放在後面,請記得在這裡我們不可以使用陣列的.push()
,因為這樣做會改變原來的陣列,造成副作用。在 ngrx/store 並不會強制您這樣做,所以這是程式的紀律問題,有了這個紀律,往後系統複雜時,就比較不會產生莫名其妙的 Bug.
在刪除一筆資料時(REMOVE_MESSAGE),我們使用了 .filter()
,這個陣列的 Operator 是可以使用的,因為它會產生新的陣列,不會改變原來的陣列。
接著我們將這個 Reducer 註冊給 Store 使用,整個程式如下
interface Action {
type: string;
payload?: any
}
class Dispatcher extends Rx.Subject<Action> {
dispatch(act) {
//console.log('got dispatch ', act.type);
this.next(act);
}
}
class Store extends Rx.BehaviorSubject<Action> {
constructor(private dispatcher, private reducer, initialState) {
super(initialState);
this.dispatcher
.do((v) => { /*console.log('do some effect for', v.type) */})
.scan((s, v) => this.reducer(s, v), initialState)
.subscribe(state => {
super.next(state); // new state, push to subscriber
});
}
dispatch(act) { // delegate to dispatcher
this.dispatcher.dispatch(act);
}
// override next to allow store subscribe action$
next(act) {
this.dispatcher.dispatch(act);
}
}
// reducer
const message = (state = [], action) => {
switch (action.type) {
case 'ADD_MESSAGE':
return [...state, action.payload];
case 'REMOVE_MESSAGE':{
return state.filter(msg => msg.id != action.payload);
}
default:
return state;
}
}
// instanciate new store
const initialState = [];
const dispatcher = new Dispatcher();
const store = new Store(dispatcher, message, initialState);
// add subscriber
const sub1 = store.subscribe(v => console.log('messages ===> ', v));
// start dispatch action
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 1, message: 'First Message'}});
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 2, message: 'Seconde Message'}});
store.dispatch({type: 'REMOVE_MESSAGE', payload: 2});
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 3, message: 'Third Message'}});
它的輸出如下
上面的程式架構還是延續原本的 Store 大架構,我們增加了一個參數 reducer,dispather 會將新的 Action 交給 .scan()
這個 Operator, 由 reducer 負責將就的狀態跟Action 處理完產生新的狀態。因為使用了 .scan()
,整個循環會一直持續使用這個 reducer 來處理 Action,而且會用到前面的狀態。
實際上的 Reducer 會更複雜,這裡我們只有單一個狀態,單一個 Reducer, 那麼如果兩個以上的 Reducer 呢?還有如何處理狀態樹呢?我們接下來看。