我們的 Store 內部大致完成,但是對於客戶 (Subscriber),我們還少了介面,在 ngrx/store 裡,給客戶的介面是用 store.select()
,.select()
最主要的目的是提供 Subscriber 它所要的狀態,而不是整個狀態樹,它是一個 Observable,客戶 subscribe()
後會被 push
新的狀態。
我們知道 Store 是一個 BehaviorSubject
,它隨時會紀錄最新的狀態樹,而狀態樹用 key
來做分類,所以做 select()
其實很簡單,只要用一個 .map()
Operator如下:
class Store extends Rx.BehaviorSubject<Action> {
// 省略 ...
select(key:string) {
return this.map(state=> state[key]);
}
}
// 省略 ...
// add subscriber
const sub1 = store.select('login')
.subscribe(s => console.log('login status => ', s));
const sub2 = store.select('messages')
.subscribe(s => console.log('messages => ', s));
// start dispatch action about login
store.dispatch({type: 'LOGIN'});
store.dispatch({type: 'LOGOUT'});
store.dispatch({type: 'LOGIN'});
// start dispatch about messages
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 1, msg: 'First Message'}});
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 2, msg: 'Second Message'}});
store.dispatch({type: 'REMOVE_MESSAGE', payload: 1});
這裡假設 sub1
只對 login
有興趣,而 sub2
只對 messages
有興趣,但是我們來看一下輸出
這種做法的話,每次 store dispatch
一個 Action 時,sub1
跟 sub2
都會得到一個值,而對於 sub2
而言,login
有關的 Action 並不會帶給 messages
新的值, 所以一剛開始時,sub2
一直收到 []
的狀態,這不是它想要的。
在 Observable 中有一個 Operator .distinctUntilChange()
,它會根據新的 .next()
做過濾,如果值不變,它會將它過濾掉,不會丟給 Subscriber。
我們加入 .distinctUntilChange()
在 .select()
class Store extends Rx.BehaviorSubject<Action> {
// 省略 ...
select(key:string) {
return this.map(state=> state[key])
.distinctUntilChanged();
}
}
結果如下
果然清爽許多,sub1
跟 sub2
都會得到它們想要的結果。
interface Action {
type: string;
payload?: any
}
class Dispatcher extends Rx.Subject<Action> {
dispatch(act) {
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);
}
select(key:string) {
return this.map(state=> state[key])
.distinctUntilChanged();
}
}
const login = (state = false, action) => {
switch (action.type) {
case 'LOGIN':
return true;
case 'LOGOUT':
return false;
default:
return state;
}
}
const messages = (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;
}
}
const myReducer = {login, messages}; // object of two reducer functions
const combineReducer = reducers => (state, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key]=reducers[key](state[key], action);
//console.log('nextState is ', nextState);
return nextState;
}, {})
}
const rootReducer = combineReducer(myReducer);
// instanciate new store with initialstate
const initialState = {login: false, messages: []};
const dispatcher = new Dispatcher();
const store = new Store(dispatcher, rootReducer, initialState);
// add subscriber
const sub1 = store.select('login')
.subscribe(s => console.log('login status => ', s));
const sub2 = store.select('messages')
.subscribe(s => console.log('messages => ', s));
// start dispatch action about login
store.dispatch({type: 'LOGIN'});
store.dispatch({type: 'LOGOUT'});
store.dispatch({type: 'LOGIN'});
// start dispatch about messages
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 1, msg: 'First Message'}});
store.dispatch({type: 'ADD_MESSAGE', payload: {id: 2, msg: 'Second Message'}});
store.dispatch({type: 'REMOVE_MESSAGE', payload: 1});
至此,我們已經將 Observable 跟 store 介紹差不多了, ngrx/store 有三大主軸, Angular (ng), Observable (rx) 跟 Store (flux),接下來我們要進入 Angular 主題來應用 ngrx/store。