|
像傳統(tǒng)數(shù)據(jù)一樣,代表著應(yīng)用的記錄點(diǎn),你的Store可以被認(rèn)為是客戶端"真實(shí)的數(shù)據(jù)來源" 或 數(shù)據(jù)庫。在設(shè)計應(yīng)用時都遵守一個Store的約定,Store任何時刻的存儲快照都將只是呈現(xiàn)應(yīng)用程序的完整狀態(tài)。 一個單一的、不可變的狀樹,只有通過顯式定義和調(diào)度才能更新。 中心化,不可變狀態(tài) ReducersStore應(yīng)用程序的第二個組成部分是reducers。A2 reducer 是一個 a3純函數(shù),前一個狀態(tài)和一個與事件相關(guān)聯(lián)的類型和可選數(shù)據(jù)(playload)的Action。使用以前的說法是,如果Store被認(rèn)為是客戶端的數(shù)據(jù)庫, 則reducers可以被認(rèn)為是數(shù)據(jù)庫中數(shù)據(jù)表。Reducers代表著應(yīng)用程序的部分或狀態(tài)片段,應(yīng)相應(yīng)的進(jìn)行結(jié)構(gòu)化和組合。 Reducer 接口2export interface Reducer<T> {
(state: T, action: Action): T;
}
A3函數(shù)的返回值類型由其輸入值的類型決定的。 一個簡單的Reducerexport const counter: Reducer<number> = (state: number = 0, action: Action) => {
switch(action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state -1;
default:
return state;
}
}
ActionsStore包含了我們應(yīng)用程序的state和Reducers的輸出部分,但是當(dāng)狀態(tài)需要更新時,我們?nèi)绾闻creducers通信呢?這是actions4,在Store應(yīng)用程序中,所有導(dǎo)致狀態(tài)更新的用戶交互都必須以actions的形式表示。所有與用戶相關(guān)的事件都被分派為action,經(jīng)過Store的4個action通道,然后輸出一個新的狀態(tài)表示。每次調(diào)度一個action時都會發(fā)生這個過程,留下應(yīng)用程序狀態(tài)隨時間變化的完整的,可序列話的表示形式。 Action接口4export interface Action {
type: string;
payload?: any;
}
派發(fā)Action的流水線5Actions簡單示例//沒帶數(shù)據(jù)的action
dispatch({type: 'DECREMENT'});
//帶數(shù)據(jù)的action
dispatch({type:ADD_TODO, payload: {id: 1, message: 'Learn ngrx/store', completed: true}});
數(shù)據(jù)投影最后,我們需要從Store中提取、組合和投影數(shù)據(jù)以顯示在我們的視圖中。因?yàn)镾tore本身是可觀察的,所以我們可以訪問你習(xí)慣的典型JS集合操作(map, filter, reduce等)以及強(qiáng)大的基于RxJS的可觀察操作符。這將使得將Store數(shù)據(jù)分割成你希望很容易的投影。 狀態(tài)投影//最簡單的示例,從state獲取people
store.select('people');
//合并多個state
Observable.combineLatest(
store.select('people'),
store.select('events'),
(people, events) => {
// 在此投影
}
)
Not Your Classic Angular在上一節(jié)中,我提到了在開發(fā)應(yīng)用程序時遵守的約定。在傳統(tǒng)的設(shè)置和工作流程,你已經(jīng)習(xí)慣了嗎,這是什么意思?讓我們一起來看看。 如果你是從Angular1轉(zhuǎn)過來的,你會很熟悉數(shù)據(jù)的雙向綁定6??刂破靼裮odel綁定到視圖,反之亦然。這種方法的問題出現(xiàn)在你的視圖變得更新復(fù)雜時,需要控制器和指令來管理并表示重要的狀態(tài)隨時間的變化。這很快變成一個噩夢,無論是推理還是調(diào)度,因?yàn)橐粋€變化會影響另一個變化,另一個又影響另一個......等等。 Store提升了單向數(shù)據(jù)流7和顯式調(diào)度操作的概念。的感受狀態(tài)更新都緩存在組件中,委托給reducer。在應(yīng)用程序中啟動狀態(tài)更新的唯一辦法是通過調(diào)度操作,對應(yīng)于特定的reducer案例。這不僅使你應(yīng)用程序的狀態(tài)改變變得簡單,因?yàn)楦率羌显谝黄鸬模鼤诔霈F(xiàn)錯誤時留下清晰的線索。 雙向數(shù)據(jù)綁定66 Two-Way Data Binding單向數(shù)據(jù)綁定不使用Store的Counter示例(在線演示) @Component({
selector:'counter',
template: `
<div class='counter'>
<button (click)='increment()'>+</button>
<button (click)='decrement()'>-</button>
<h3>{{counter}}</h3>
</div>
`
})
export class Counter {
counter = 0;
increment() {
this.counter += 1;
}
decrement() {
this.counter -= 1;
}
}
使用Store的Counter示例(演示) @Component({
selector: 'counter',
template: `
<div class='content'>
<button (click)="increment()">+</button>
<button (click)="decrement()">-</button>
</div>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class Counter {
counter$: Observable<number>;
constructor(
private store: Store<number>
){
this.counter$ = this.store.select('counter');
}
increment(){
this.store.dispatch({type:'INCREMENT'});
}
decrement(){
this.store.dispatch({type:'DECREMENT'});
}
}
Store的優(yōu)勢在整個概述中,我們簡要介紹了利用Store在一種典型的Angular 1風(fēng)格方法的優(yōu)勢,現(xiàn)在讓我們發(fā)一點(diǎn)時間來回顧一下。為什么要花時間在這個特定的庫,構(gòu)建和曲線上投資呢?Store的優(yōu)勢是狀態(tài)中心化,性能,測試。 中心化,狀態(tài)不可變所有相關(guān)應(yīng)用程序的狀態(tài)都緩存在一個位置。這樣可以很容易地跟蹤問題,因?yàn)殄e誤時的狀態(tài)快照可以提供重要的見解,并且可以輕松的重新重現(xiàn)這個問題。這也使得眾多困難問題,例如在Store應(yīng)用程序的上下文中撤消/重做某一步驟,并且實(shí)現(xiàn)了更強(qiáng)大的功能的工具。 性能由于狀態(tài)集合中應(yīng)用程序的頂層,因?yàn)閿?shù)據(jù)更新可以通過組件依賴于Store。Angular構(gòu)建如這樣的數(shù)據(jù)流布置進(jìn)行優(yōu)化,并且可以在組件依賴于沒有發(fā)布新值的Observables的情況下禁用變化檢測。在最佳的緩存解決方案中,這將是絕大多數(shù)組件。 測試所有狀態(tài)更新都是在recudes中處理的,它們是純函數(shù)。純函數(shù)測試非常簡單,因?yàn)樗皇禽斎?,反對輸出。這樣可以測試應(yīng)用程序中最關(guān)鍵的方面,而無需使用mock,或其他的測試技巧,可以使測試復(fù)雜且容易出錯。 工具與生態(tài)系統(tǒng)中心化的,不可變的狀態(tài)還可以實(shí)現(xiàn)更強(qiáng)大的工具。一個這樣的盒子是ngrx開發(fā)工具,它提供了action和狀態(tài)變化的歷史,允許在開發(fā)過程中進(jìn)行8次遍歷。Store提供的模式還允許一個易于實(shí)現(xiàn)中間件的豐富的生態(tài)系統(tǒng)。因?yàn)镾tore在分派action之前和之后都提供一個入口點(diǎn),所以應(yīng)用程序減少,如同步片狀態(tài)到本地Store,高級日志記錄和實(shí)現(xiàn)sagas這樣的問題可以通過快速包和幾行代理來解決。這個生態(tài)系統(tǒng)只會在未來幾個月內(nèi)增長。 操作調(diào)度action和狀態(tài)更改的歷史 ,以模擬應(yīng)用程序交互的時間點(diǎn)。 @ngrx/store的構(gòu)建模塊在構(gòu)建Store應(yīng)用程序之前,首先來看看構(gòu)建@ngrx/store的RxJS概念。首先理解這些概念,我們將來可以更有效地利用這個庫。要詳細(xì)說明下面的每個主題,請查看這些額外的資源。 聲明:Mike Ryan和Rob Wormald的實(shí)際@ngrx/store代碼顯著更強(qiáng)大。這些示例旨在演示涉及的RxJS概念,并從庫中移出"magic" Subject/Dispatch的探索Rx的信使們,你告訴我,我會告訴他們的..... (演示) @ngrx/store的兩個支柱,Store和Dispatcher都擴(kuò)展了RxJS主題。主題即是觀察者(Observables)和觀察者(Observers),這意味著你可以訂閱Subject,但也可以將主題訂閱源。在高級別科目可以被認(rèn)為是信使或代理人。 因?yàn)镾ubject是Observables,你可以 "next" 或直接將值傳遞到流中。然后,該Subject的訂閱將被通知發(fā)出值。在Store的上下文中,這些用戶可能是一個Angular 服務(wù), 組件或需要訪問應(yīng)用程序狀態(tài)的任何內(nèi)容。 訂閱主題//創(chuàng)建一個主題
const mySubject = new Rx.Subject();
//添加訂閱者
const subscriberOne = mySubject.subscribe(val => {
console.log('***SUBSCRIBER ONE***',val);
});
const subscriberTwo = mySUbject.subscribe(val => {
console.log('***SUBSCRIBER TWO***',val);
});
//發(fā)射subject的值到observers
mySubject.next('FIRST VALUE!');// ***SUBSCRIBER ONE*** FIRST VALUE! ** SUBSCRIBER TWO*** FIRST VALUE!
mySubject.next('SECOND VALUE!');//***SUBSCRIBER ONE*** SECOND VALUE! ***SUBSCRIBER TWO*** SECOND VALUE
在Store或Redux中,將action發(fā)送到應(yīng)用程序中的Store是一種慣例。為了維護(hù)此API,Dispatcher擴(kuò)展至Subject,將派生方法作為傳遞添加到傳統(tǒng)的下一個方法。這被用于將值發(fā)送到Subject中,然后將這些值發(fā)送給子對象。 將Dispatcher繼承自Subject/*
redux/ngrx-store 有一個dispatcher的概念,或者是面向應(yīng)用程序Store發(fā)送操作的方法允許擴(kuò)展Rx.Subject與我們的Dispatcher類來維護(hù)熟悉的術(shù)語。
*/
//從Subject中繼承
class Dispatcher extends Rx.Subject {
dispatcher(value: any): void{
this.next(value);
}
}
//創(chuàng)建一個dispatcher(只是一個包含next的SUbject方法)
const dispatcher = new Dispatcher();
//添加訂閱
const subscribeOne = dispatcher.subscribe(val => {
console.log('***SUBSCRIBER ONE***', val);
});
const subscribeTwo = dispatcher.subscribe(val => {
console.log('***SUBSCRIBER TWO***', val);
});
//將值發(fā)射到observers
dispatcher.dispatch('FIRST DISPATCHED VALUE!');
dispatcher.dispatch('SECOND DISPATCHED VALUE!');
BehaviorSubject/Store探索與Subject類似,但你說的最后一件事是什么?... (演示) 雖然Subject作為dispatcher完美地工作,但它們有一個問題可以防止他們適合Store。訂閱Subject時,只接收訂閱后發(fā)出的值。在不斷添加和刪除組件的環(huán)境中,這是不可接受的,在訂閱時需要應(yīng)用程序Store的最新的按需狀態(tài)部分。 Subjects只接受訂閱后發(fā)出的值 /*
現(xiàn)在我們有一個dispatcher, 讓我們創(chuàng)建我們的Store來接收已經(jīng)發(fā)送的action。
*/
class FirstStore extends Rx.Subject{}
const myFirstStore = new FirstStore();
//添加訂閱者
const subscriberOne = myFirstStore.subscribe(val => {
console.log('***SUBSCRIBER ONE***', val);
});
const subscriberTwo = myFirstStore.subscribe(val => {
console.log('***SUBSCRIBER TWO***', val);
});
//現(xiàn)在,讓超級dispatcher發(fā)布值到store
myFirstStore.next('FIRST VALUE!');
/*
我們在添加一個訂閱者。
由于我們第一次實(shí)施Store是一個subject,訂閱者只能看到發(fā)布的價值*AFTER*他們訂閱之后。在這種情況下,訂閱者3將不了解'FIRST VALUE!'
*/
const subscriberThree = myFirstStore.suscribe(val => {
console.log('***SUBSCRIBER THREE***', val);
});
幸運(yùn)的是,RxJS為Subject處理這個問題提供了BehaviorSubject。 即BehviorSubject 封裝了Subject的所有功能,但也可以在訂閱后將改后發(fā)布的值返回給訂閱都。這意味著組件和服務(wù)將始終可以訪問最新(或初始值)應(yīng)用程序狀態(tài)和所有將來的更新。 BehaviorSubject訂閱接收上一次發(fā)布的值/*
因?yàn)槲覀兊慕M件需要查詢當(dāng)前狀態(tài),所以BehaviorSubject更適合Store。BehaviorSubjects具有Subject的所有功能,還允許設(shè)置初始值,以及在訂閱時將所接收的最后一個值輸出給所有觀察者。
*/
class Store extends Rx.BehaviorSubject {
constructor(initialState: any){
super(initialState);
}
}
const store = new Store('INITIAL VALUE');
//添加一些訂閱者
const storeSubscriberOne = store.subscribe(val => {
console.log('***STORE SUBSCRIBER ONE***', val);
});
//為了演示,手動發(fā)布值到store
const storeSubscriberTwo = store.subscribe(val => {
console.log('***STORE SUBSCRIBER TWO***', val);
});
//在'FIRST VALUE!' 發(fā)布之后添加另一個訂閱者
//輸出:***STORE SUBSCRIBER THREE*** FIRST STORE VALUE!
const subscriberThree = store.subscribe(val => {
console.log('***STORE SUBSCRIBER THREE***', val);
});
Store + Dispatcher數(shù)據(jù)流單狀態(tài)樹和單向數(shù)據(jù)流在Angular ... (演示) 為了store的上下文正常運(yùn)行,dispatcher仍然需要一些工作。在Store應(yīng)用程序中,所有dispatch的action必須通過特定的管道傳遞,才能將新的狀態(tài)表示傳遞到store中,并發(fā)送給所有觀察者。你可以將此視為工廠裝配線,在這種情況下,線上的站是pre-middleare->reducers->post->middleware->store。 這個流水線的創(chuàng)建是在創(chuàng)建時dispatch傳遞給store處理的。然后,store下一個方法被覆蓋,以便將新的狀態(tài)表示傳遞到store之前,首先將所有的action都dispatch管道。這也允許通過dispatch匯集接收到的action。 現(xiàn)在,中間件和reducers的實(shí)現(xiàn)將被刪除。 將Dispatcher與Store關(guān)聯(lián)一起/*
所有action都應(yīng)通過管道,然后新計算的狀態(tài)通過store。
1.) Dispatched Action
2.) Pre-Middleware
3.) Reducers (return new state)
4.) Post-Middleware
5.) store.next(newState)
*/
class Dispatcher extends Rx.Subject{
dispatcher(value: any): void{
this.next(value);
}
}
class Store extends Rx.BehaviorSubject{
constructor(
private dispatcher,
initialState
){
super(initialState);
/*
所有dispatch的action在通過新狀態(tài)之前 通過action管道傳遞到store
*/
this.dispatcher
//pre-middleware
//reducers
//post-middleware
.subscribe(state => super.next(state));
}
//首先通過分派action到管道并委托給store.dispatch
dispatch(value){
this.dispatcher.dispatch(value);
}
//覆蓋store允許直接訂閱action注通過store
next(value){
this.dispatcher.dispatch(value);
}
}
const dispatcher = new Dispatcher();
const store = new Store(dispatcher,'INITIAL STATE');
const subscriber = store.subscribe(val => console.log('VALUE FROM STORE: ${val}'));
/*
所有分派action首先流經(jīng)管道,計算新狀態(tài)然后傳遞到store??偨Y(jié)一下,我們的理想行為分派action->premiddleware->reducers->post-middleware->store.next(newState)
*/
//兩種方法在幕后都是相同的
dispatcher.dispatch('DISPATCHED VALUE!');
store.dispatch('ANOTHER DISPATCHED VALUE!');
const actionStream$ = new Rx.Subject();
/*
覆蓋store下一個方法允許我們將store直接訂閱到action流,提供與手動調(diào)用store.dispatch或dispatcher.dispatch相同的行為
*/
actionStream$.subscribe(store);
actionStream$.next('NEW ACTION!');
什么是Reducer?像雪球一樣下滑,reducer通過迭代累加... (演示) Reducers是基于任何store或Redux的應(yīng)用基礎(chǔ),描述基于分派action類型的狀態(tài)部分及其潛在轉(zhuǎn)換。你的reducer的組合是在任何給定時間組成應(yīng)用程序狀態(tài)的表示。 在討論如何創(chuàng)建和實(shí)現(xiàn)reducers之前 , 我們先來看看reduce函數(shù)。reduce需要一個數(shù)組,根據(jù)累加值和當(dāng)前值運(yùn)行一個函數(shù),在完成后將數(shù)組遞減一個值。你可以把reducers看成一個滾雪而下的雪橇,每一次變革都會變得很大。以相同的方式,減少reduce是通過迭代定義的函數(shù)應(yīng)用于當(dāng)前值的結(jié)果。 標(biāo)準(zhǔn)的Reduce/*
你可以想一下滾雪球的場景。每一次翻滾都會累加質(zhì)量和體積直到到達(dá)底部。reduce也類似,返回的值傳遞給所有提供函數(shù)的下一個調(diào)用,直到源數(shù)組中的所有值都耗盡為止。讓我們看看一些鞏固概念的盒子。
*/
const numberArray = [1,2,3];
/*
1.) accumulator:1, current:2
2.) accumulator:3, current:3
Final: 6
*/
const total = numberArray.reduce((accumulator, current) => accumulator + current);
console.log('***TOTAL***:',${total});
//reduce操作的對象
const personInfo = [{name:'Joe'},{age:31},{birthday:'1/1/1985'}];
/*
1.) accumulator: {name: 'Joe'}, current: {age: 31}
2.) accumulator: {name: 'Joe', age:31}, current: {birthday: '1/1/1985'}
Final: {name: 'Joe', age:31, birthday: '1/1/1985'}
*/
const fullPerson = personInfo.reduce(accumulator, current) => {
return Object.assign({}, accumulator, current);
}
console.log('*** FULL PERSON***:',fullPerson);
const personInfoStart = [{name:'Joe'},{age: 31},{birthday:'1/1/1985'}];
/*
1.) accumulator: {favoriteLangue: 'JavaScript'}, current: {name: 'Joe'}
2.) accumulator: {favoriteLangue: 'JavaScript', name: 'Joe'}, current: {age: 31}
3.) accumulator: {favoriteLange: 'JavaScript', name: 'Joe', age: 31}, current: {birthday: '1/1/1985'}
Final: {favoriteLangue: 'JavaScript', name: 'Joe', age: 31, birthday: '1/1/1985'}
*/
const fullPersonStart = personInfo.reduce((accumulator, current) => {
return Object.assign({}, accumulator, current);
},{favoriteLangue:'JavaScript'});
console.log('***FULL PERSON START:', fullPersonStart);
受Redux的啟發(fā),@ngrx/store具有操縱特定狀態(tài)的Reducer功能的概念。Reducer接受一個state和action作為參數(shù),暴露一個switch語句(一般來說,盡管 這可以通過多種方式處理)定義reducer所涉及的action類型。每次分派一個action時,將調(diào)用注冊到store的每個reducer(通過根reducer, 在應(yīng)用程序引導(dǎo)時在provideStore中創(chuàng)建),傳遞該狀態(tài)片段(累加器)的當(dāng)前狀態(tài)和已分派的action。如果reducer沒有被注冊來處理該action類型,則將執(zhí)行適當(dāng)?shù)臓顟B(tài)計算和狀態(tài)輸出的表示。如果 沒有那么該部分的當(dāng)前 狀態(tài)將被返回。這是Store和Redux的狀態(tài)管理核心。 Store / Redux 風(fēng)格的Reducer// Redux風(fēng)格的Reducer
const person = (state = {}, action ) => {
switch(action.type){
case 'ADD_INFO':
return Object.assign({}, state, action.payload);
default:
return state;
}
}
const infoAction = {type: 'ADD_INFO', payload: {name:'Brian', framework:'Angular'}};
const anotherPersonInfo = person(undefined, infoAction);
console.log('***REDUX STYLE PERSON***:', anotherPersonInfo);
//添加其他reducer
const hoursWorked = (state = 0, action) => {
switch(action.type) {
case 'ADD_HOUR':
return state + 1;
case 'SUBTRACT_HOUR':
return state -1;
default:
return state;
}
}
//組合Reducers更新數(shù)據(jù)
const myReducers = { person, hoursWorked};
const combineReducers = reducers => (state = {}, action) => {
return Object.keys(reducers).reduce((nextState, key) => {
nextState[key] = reducers[key](state[key],action);
return nextState;
}, {});
};
/*
這讓我們大部的方式在那里,但真正希望我們想要的是第一個和第二個的值累加隨著action隨著時間推移。幸運(yùn)的是,RxJS為這處情況提供了完美的操作符,將在下一課中討論。
*/
const rootReducer = combineReducers(myReducers);
const firstState = rootReducer(undefined, {type:'ADD_INFO', payload:{name: 'Brian'}});
const secondState = rootReducer({hoursWorked: 10, person: {name: 'Joe'}},{type:'ADD_HOUR'});
console.log('***FIRST STATE***:',firstState);
console.log('***SECOND STATE***:',secondState);
通過根Reducer分派action9使用scan操作符聚合狀態(tài)類似于reduce,但值隨著時間的推移累加。。。 scan操作符以類似的方式扮作reduce,除了累加器隨時間保持,或直接scan應(yīng)用的可觀察完成。例如,當(dāng)分派action和新的狀態(tài)輸出時,scan函數(shù) 中的累加器將始終是狀態(tài)的最后一個輸出表示形式。這減輕了需要維護(hù)store中的狀態(tài)副本以傳遞給我們的reducer。 scan操作符的基本示例const testSubject = new Rx.Subject();
//scan示例,從0開始每次累加
const basicScan = testSubject.scan((acc, curr) => acc+ curr, 0);
// 記錄累加值
const subscribe = basicScan.subscribe(val => console.log('Accumulated total:', val));
//傳遞值到我們的testSubject,并累加當(dāng)前值
testSubject.next(1);//1
testSubject.next(2);//2
testSubject.next(3);//3
const testSubjectTwo = new Rx.Subject();
// scan示例隨著時間的推移建立對象
const objectScan = testSubjectTwo.scan((acc, curr) => Object.assign({}, acc,curr), {});
// 記錄累加值
const subscribe = objectScan.subscribe(val => console.log('Accumulated object:', val));
//傳遞值到testSubject,添加屬性到一個新對象
testSubjectTwo.next({name: 'Joe'});
testSubjectTwo.next({age: 30});
testSubjectTwo.next({favoriteFramework: 'Angular 2'});// {name: 'Joe', age: 30, favoriteFramework: 'Angular 2'}
為了在應(yīng)用程序store中使用scan,它只需要操作符應(yīng)用于dispatcher程序。所有分派的action都將通過scan,調(diào)用具有當(dāng)前state和action組合的reducer,輸出新的狀態(tài)表示。然后,就的應(yīng)用程序狀態(tài)被關(guān)閉,或被推送到store,并發(fā)送給所有訂閱者。 使用scan做store存儲class Store extends Rx.BehaviorSubject{
constructor(
private dispatcher,
private reducer,
initialState = {}
){
super(initialState);
this.dispatcher
// pre-middleware?
/*
前面我們把reduce比喻成一個雪球,越滾越大(或在原來的基礎(chǔ)上累加)。scan也是類似行為。累加器(在這個示例中是,state)它將會繼續(xù)累加前面的數(shù)據(jù)直到終止。這使得他它成為管理應(yīng)用程序狀態(tài)的理想操作符。
*/
.scan((state, action) => this.reducer(state, action), initialState)
//post-middleware?
.subscribe(state => super.next(state));
}
// ... store implementation
}
使用let管理中間件讓我擁有整個可觀察的。。。 (let demo | store demo) 中間件已經(jīng)在ngrx/store v2中移出了。通這個部分來閱讀本書,以了解let操作符,因?yàn)樗梢耘c選擇器一起使用。 雖然大多數(shù)運(yùn)算符都是從可觀察的值傳遞出來的,但是我們可以把整個可觀察的數(shù)據(jù)傳遞出去在返回源可觀察數(shù)據(jù)之前,這允許有機(jī)會處理額外的操作符和功能。雖然這可能看起來像一個小小的細(xì)微差別,但它完全適合于中間件或選擇器(稍后討論)的情況,消費(fèi)者想要定義一個可利用的,可重復(fù)使用的代碼塊,以插入到可觀察鏈中的特定時隙。 let 的基本功能const myArray = [1,2,3,4,5];
const myObservableArray = Rx.Observable.fromArray(myArray);
const test = myObservableArray
.map(val => val +1)
//這里會失敗,let 與多數(shù)的操作符不一樣
//.let(val => val + 2)
.subscribe(val => console.log('VALUE FROM ARRAY:', val));
const letTest = myObservableArray
.map(val => val +1)
//let 操作符擁有整個observable
.let(obs => obs.map(val => val +2))
.subscribe(val => console.log('VALUE FROM ARRAY WITH let :', val));
//let 操作符提供靈活度,添加多個操作符到源observable然后返回
const letTestThree = myObservableArray
.map(val => val +1)
//let 操作符擁有整個observable
.let(obs => obs
.map(val => val +2)
.filter(val => val % 2 === 0)
)
.subscribe(val => consle.log('let WITH MULTIPLE OPERATORS:', val));
//傳遞你的函數(shù)來添加操作符到observable
const obsArrayPlusYourOperators = (yourAppliedOperators) => {
return myObservableArray
.map(val => val +1 )
.let(yourAppliedOperators)
};
const addTenThenTwenty = obs => obs.map(val => val + 10).map(val => val + 20);
const letTestFour = obsArrayPlusYourOperators(addTenThenTwenty)
.subscribe(val => console.log('let FROM FUNCTION:', val));
let 操作符非常適合@ngrx/store中間件,因?yàn)橛脩粼趓educer輸出狀態(tài)之前 或之后添加自定義功能需要一個入口點(diǎn),這是在@ngrx/store中如何應(yīng)用前后中間件的基礎(chǔ)。 添加let 操作符到中間件的入口class Store extends Rx.BehaviorSubject{
constructor(
private dispatcher,
private reducer,
preMiddleware,
postMiddleware,
initialState = {}
){
super(initialState);
this.dispatcher
//let 操作符接受整個源observable,返回一個新的observable
//@ngrx/store 組成中間件,所以你可以提供多個功能
//在我們下面的示例中,會接受一個前件和一個后件
//中間件標(biāo)識:(obs) => obs
.let(preMiddleware)
.scan((state, action) => this.reducer(state,action),initialState)
.let(postMiddleware)
.subscribe(state => super.next(state));
}
// ... store implementation
}
const preMiddleware = obs => { return obs.do(val => console.log('ACTION:', val))};
const postMiddleware = obs => {return obs.do(val => console.log('STATE:', val))};
... create store supplying middleware
我們來回顧一下之前的內(nèi)容:
這是store內(nèi)部工作的要點(diǎn)。 使用map 操作符修改state我會把復(fù)雜的片段... (demo) 從集合中投影數(shù)據(jù)的基石功能是map。map將指定的函數(shù) 應(yīng)用于一項(xiàng),返回該薦的新結(jié)果。因?yàn)閼?yīng)用程序狀態(tài)的鍵/值對象映射,所以提供一個輔助函數(shù) 來簡單的返回基于字符 串或任何其他相關(guān)選擇器的請求的狀態(tài)片段。 通過map操作符轉(zhuǎn)換stateclass Dispatcher extends Rx.Subject{
dispatch(value: any) : void{
this.next(value);
}
}
class Store extends Rx.BehaviorSubject{
constructor(
private dispatcher,
private reducer,
preMiddleware,
postMiddleware,
initialState = {}
){
super(initialState);
this.dispatcher
.let(preMiddleware)
.scan((state,action) => this.reducer(state, action), initialState)
.let(postMiddleware)
.subscribe(state => super.next(state));
}
//map 可以很容易的選擇組件所需的狀態(tài)片段
//這個一種簡單的輔助功能,使得state的抓取部分更加簡潔
select(key:string){
return this.map(state => state[key]);
}
// ... store implemenetation
}
//... create store
//使用store的select輔助
const subscriber = store
.select('person')
.subscribe(val => console.log('VALUE OF PERSON:', val));
使用distinctUntilChanged管理狀態(tài)更新改變之前不要調(diào)用我 (distinctUntilChanged demo | store demo) 我們應(yīng)用程序中的每個視圖都只關(guān)心自己的狀態(tài)片段。由于性能原因,我們不希望從所選狀片段中發(fā)出新值,除非已進(jìn)行更新。幸運(yùn)的是,對于我們來說,RxJS有一個很好用的操作符做這事(注意趨勢)。distinctUntilChanged操作符將僅在下一個值為唯一時基于先前發(fā)出的值發(fā)出。在數(shù)字和字符串的情況下,這意味著相等的數(shù)字和字符串,在對象的情況下,如果 對象引用是相同的新對象將不會被發(fā)出。 在基本類型與引用類型上使用distinctUntilChanged//只會輸出唯一的值,同時是基于最新發(fā)出的值
const myArrayWithDuplicateInARow = new Rx.Observable
.fromArray([1,1,2,2,3,1,2,3]);
const distinctSub = myArrayWithDuplicatesInARow
.distinctUntilChanged()
//output: 1,2,3,1,2,3
.subscribe(val => console.log('DISTINCT SUB:',val));
const nonDistinctSub = myArrayWithDuplicatesInARow
//output: 1,1,2,2,3,1,2,3
.subscribe(val => console.log('MON DISTINCT SUB:', val));
const sampleObject = {name: 'Test'};
const myArrayWithDuplicateObjects = new Rx.Observable.fromArray([sampleObject,sampleObject,sampleObject]);
//只輸出唯一的對象,同時是基于最新發(fā)出的值
const nonDistinctObjects = myArrayWithDuplicateObjects
.distinctUntilChanged()
//output: 'DISTINCT OBJECTS: {name: 'Test'}
.subscribe(val => console.log('DISTINCT OBJECTS:',val));
回想一下,store的reducers始終具有默認(rèn)的情況,如果與分派的action不相關(guān),則返回上一個狀態(tài)。這意味著,在應(yīng)用程序中選擇狀態(tài)片段時,除非更新了特定的片段,否則不會接收更新。這有助于使你的Store應(yīng)用程序更有效率。 在Store中使用distinctUntilChangedclass Dispatcher extends Rx.Subject {
dispatch(value:any) : void{
this.next(value);
}
}
class Store extends Rx.BehaviorSubject{
constructor(
private dispatcher,
private reducer,
preMiddleware,
postMiddleware,
initialState = {}
){
super(initialState);
this.dispatcher
.let(preMiddleware)
.scan((state, action) => this.reducer(state,action), initialState),
.let(postMiddleware)
.subscribe(state => super.next(state));
}
/*
distinctUntilChanged僅在輸出不同時才發(fā)出新值,最后發(fā)出的值。在下面的示例中,distinctUntilChanged操作符的可觀察值將發(fā)送一個較小的值,而另一個僅使用map操作符。
*/
select(key:string){
return this.map(state => state[key])
.distinctUntilChanged();
}
}
// add reducers
// configure store
const subscriber = store
//使用distinctUntilChanged
.select('person')
.subscribe(val => console.log('PERSON WITH DISTINCTUNTILCHANGED:', val));
const subscriberTwo = store
//沒有使用distinctUntilChanged, 將會打印數(shù)據(jù)
.map(state => state.person)
.subscribe(val => console.log('PERSON WITHOUT DISTINCTUNTILCHANGED:', val));
// dispatch a few actions
dispatcher.dispatch({
type:'ADD_INFO',
payload:{
name:'Brian',
message:'Exporing Reduce!'
}
});
// person不會被改變
dispatcher.dispatch({
type:'ADD_HOUR'
});
演練一個簡單的應(yīng)用程序我們將要創(chuàng)建的示例程序10是一個簡單入門的應(yīng)用。用戶應(yīng)該能夠輸入?yún)⒓诱呒捌淇腿说牧斜?,跟蹤認(rèn)證確認(rèn)出席者,通過特定標(biāo)準(zhǔn)過濾與會者,并快速查看有關(guān)活動的重要統(tǒng)計信息。在整個應(yīng)用程序的創(chuàng)建過程中,我們將探討@ngrx/stre的核心概念,并討論流行的模式和最佳做法。 在每個部分上方提供了兩個鏈接 ,工作和完成課程。如果 你希望按照概念進(jìn)行編碼,則每個課程的開始時,'工作'鏈接 將取提取。否則,"完成課程"鏈接 允許你從當(dāng)前課程的終點(diǎn)開始。 不多說了,我們開始吧。 Party Planner應(yīng)用程序結(jié)構(gòu)圖10設(shè)置第一個Reducer(Work Along | Completed Lesson) Reducer是你應(yīng)用程序的基礎(chǔ)。隨著應(yīng)用程序緩存維護(hù)狀態(tài),reducer是動態(tài)調(diào)度時action和輸出新狀態(tài)表示的主要內(nèi)容。每個reducer應(yīng)該集中在與數(shù)據(jù)庫的表類似的特定部分或狀態(tài)片段上。 創(chuàng)建reducer是很簡單的一旦習(xí)慣了一個常見的成語,從不改變以前的狀態(tài),并總是返回一個新的狀態(tài)表示,當(dāng)相關(guān)的action被調(diào)度。如果 你是新來的store或Redux模式,這需要一些習(xí)慣來感覺自然。而不是使用諸如推送或重新分配先前存在的對象的變量的方法,而是依靠沒有改變的方法,如concat和Object.assign來返回新值。還困惑嗎?讓我們來看看個人在實(shí)踐中與其他的reducer有什么關(guān)系。 person的reducer需要處理5個action,移出一個person,向guest添加一個guest,從一個person中移出guest,并切換他們是否參加活動。為此, 我們將創(chuàng)建一個reducer函數(shù) ,接受以前的狀態(tài)和當(dāng)前 調(diào)度的action。然后,我們需要實(shí)現(xiàn)一個case語句,在執(zhí)行相關(guān)action時執(zhí)行正常的狀態(tài)重新計算。 Person Reducerconst details = (state, action) => {
switch(action.type){
case ADD_GUEST:
if(state.id === action.payload){
return Object.assign({}, state, {guest: state.guests + 1});
}
return state;
case REMOVE_GUEST:
if(state.id === action.payload){
return Object.assign({}, state, {guest: state.guest - 1});
}
return state;
case TOGGLE_ATTENDING:
if(state.id === action.payload){
return Object.assign({}, state,{attending: !state.attending});
}
return state;
default:
return state;
}
}
//切記,要避免reducers內(nèi)部的直接改變
export const people = (state = [], action) => {
switch(action.type){
case ADD_PERSON:
return [
...state,
Object.assign({}, {id: action.payload.id, name: action.payload, guests: 0, attending: false})
];
case REMOVE_PERSON:
return state
.filter(person => person.id !=== action.payload);
//為了簡單語句,把更新放到dtails上
case ADD_GUEST:
return state.map(person => detail(person, action));
case REMOVE_GUEST:
return state.map(person => details(person, action));
case TOGGLE_ATTENDING:
return state.map(person => details(person, action));
//當(dāng)沒有合適的action時,始終會返回先前狀態(tài)的默認(rèn)值
default:
return state;
}
}
配置Store中的actions(Work Along | Completed Lesson) 在store應(yīng)用程序中修改state的唯一方式是通過dispatch。因此 ,action的記錄應(yīng)該是很清晰的,可讀的用戶交互歷史。action通常定義為字符串常量或封裝特定操作類型的服務(wù)上的靜態(tài)字符串值。在后一種情況下,提供功能以返回給定正常輸入的適當(dāng)動作。這些方法有助于標(biāo)準(zhǔn)化你的操作,同時提供額外的類型安全性,被稱之為action的創(chuàng)建者。 對于我們的就應(yīng)用程序,我們將每個應(yīng)用程序action導(dǎo)出字符 串常量。然后,這些將用于我們reducer案例語句的關(guān)鍵字以及每個派生action的類型。 初始化Actions//Person 的Action常量 export const ADD_PERSON = 'ADD_PERSON'; export const REMOVE_PERSON = 'REMOVE_PERSON'; export const ADD_GUEST = 'ADD_GUEST'; export const REMOVE_GUEST = 'REMOVE_GUEST'; export const TOGGLE_ATTENDING = 'TOOGLE_ATTENDING'; Components Container的使用(Work Along | Completed Lesson) 在你的應(yīng)用程序中組件分為兩類,smart11和dumb。那怎么區(qū)別哪個一個是start component,哪一個是dumb component? Smart 或Container 組件應(yīng)該作為你的首層組件,或路由組件 。這些組件通??梢灾苯釉L問store或?qū)С鼋M件。Smart Component組件通過服務(wù)或直接處理視圖event和action的dispatch。同時,Smart Component組件還處理在同一視圖中從子組件發(fā)出的事件邏輯處理。 Dumb或Child組件通常僅作為顯示內(nèi)容,僅依賴于@Input參數(shù) ,以及合理的方式處理接收到的數(shù)據(jù) 。當(dāng)相關(guān)事件性在dump components時,它們通過emit操作發(fā)射出來,交由smart父組件處理。你應(yīng)用程序中將大部分由dump組件構(gòu)成,因?yàn)樗麄兪仟?dú)立的,聚焦的,并且是可重復(fù)利用的。 part planner應(yīng)用程序需要一個container component。該組件將負(fù)責(zé)將適當(dāng)?shù)臓顟B(tài)轉(zhuǎn)交給每個子組件,并根據(jù)dumb component,person-input,person-list,以及以后的part-stats發(fā)出的事件進(jìn)行調(diào)度。 Container Component@Component({
selector: 'app',
template: `
<h3>@ngrx/store Party Planner</h3>
<person-input
(addPerson)="addPerson($event)"
>
</person-input>
<person-list
[people]="people"
(addGuest)="addGuest($event)"
(removeGuest)="removeGuest($event)"
(removePerson)="removePerson($event)"
(toggleAttending)="toggleAttending($event)"
>
</person-list>
`,
directives: [PersonList, PersonInput]
})
export class App {
public people;
private subscription;
constructor(
private _store: Store
){
/*
示例沒有使用async管道,
我們將在下一小節(jié)探索async管道
*/
this.subscription = this._store
.select('people')
.subscribe(people => {
this.people = people;
});
}
//所有action狀態(tài)的改變都被轉(zhuǎn)發(fā)到reducer處理
addPerson(name) addPerson(name){
this._store.dispatch({type: ADD_PERSON, payload: {id: id(), name})
}
addGuest(id){
this._store.dispatch({type: ADD_GUEST, payload: id});
}
removeGuest(id){
this._store.dispatch({type: REMOVE_GUEST, payload: id});
}
removePerson(id){
this._store.dispatch({type: REMOVE_PERSON, payload: id});
}
toggleAttending(id){
this._store.dispatch({type: TOGGLE_ATTENDING, payload: id})
}
/*
如果我沒有使用async管道而是手動創(chuàng)建訂閱(subscriptions),則需要在ngOnDestroy中取消訂閱(unsubscribe)
*/
ngOnDestroy(){
this.subscription.unsubscribe();
}
}
Dumb Component - PersonList@Component({
selector: 'person-list',
template: `
<ul>
<li
*ngFor="let person of people"
[class.attending]="person.attending"
>
{{person.name}} - Guests: {{person.guests}}
<button (click)="addGuest.emit(person.id)">+</button>
<button *ngIf="person.guests" (click)="removeGuest.emit(person.id)">-</button>
Attending?
<input type="checkbox" [(ngModel)]="person.attending" (change)="toggleAttending.emit(person.id)" />
<button (click)="removePerson.emit(person.id)">Delete</button>
</li>
</ul>
`
})
export class PersonList {
/*
"dumb"組件接收input輸入和顯示數(shù)據(jù),發(fā)送相關(guān)事件到父組件 或container組件處理,除此之外什么也不做。
*/
@Input() people;
@Output() addGuest = new EventEmitter();
@Output() removeGuest = new EventEmitter();
@Output() removePerson = new EventEmitter();
@Output() toggleAttending = new EventEmitter();
}
Dumb Component - PersonInput@Component({
selector: 'person-input',
template: `
<input #personName type="text" />
<button (click)="add(personName)">Add Person</button>
`
})
export class PersonInput {
@Output() addPerson = new EventEmitter();
add(personInput){
this.addPerson.emit(personInput.value);
personInput.value = '';
}
}
Smart Container11 和 Dumb ComponentsAsyncPipe管道的使用(Work Along | Completed Lesson) 在Angular中AsyncPipe是一個獨(dú)特的,狀態(tài)化的管道,可以處理Observables和Promises。當(dāng)在具有Observables的模板表達(dá)式中使用AsyncPipe管道時,使用了Observable可subscribe的功能,并且在視圖中顯示emit的值。該管道還可以自動取消訂閱,從而節(jié)省了在ngOnDestroy中手動取消訂閱的麻煩。在Store應(yīng)用程序中,你會發(fā)現(xiàn)在視圖組件中所有的組件都使用了AsyncPipe管道,且還很難知道發(fā)生了什么。有關(guān)AsyncPipe是如何工作的更詳細(xì)解釋,請查看我的文章了解和使用Angular的AsyncPipe或egghead.io上AsyncPipe的免費(fèi)視頻。 在我們的模板中使用AsyncPipe是很簡單的。你可以通過async操作符傳遞Observable(或Promise),并創(chuàng)建訂閱對象,通過async管道符解析的數(shù)據(jù)更新模板。因?yàn)槲覀兪褂昧薃syncPipe管道,我們還可以從組件的構(gòu)造函數(shù)和生命周期鉤子函數(shù) ngOnDestroy中手動取消訂閱?,F(xiàn)在我們使用AsyncPipe默認(rèn)自動處理。 重構(gòu)為AsyncPipe管道@Component({
selector: 'app',
template: `
<h3>@ngrx/store Party Planner</h3>
<person-input
(addPerson)="addPerson($event)"
>
</person-input>
<person-list
[people]="people | async"
(addGuest)="addGuest($event)"
(removeGuest)="removeGuest($event)"
(removePerson)="removePerson($event)"
(toggleAttending)="toggleAttending($event)"
>
</person-list>
`,
directives: [PersonList, PersonInput]
})
export class App {
public people;
private subscription;
constructor(
private _store: Store
){
/*
people的Observable對象,在我們的模板中使用async管道,這將會被自動訂閱,使用新解析的數(shù)據(jù)顯示在我們的模板上。
當(dāng)組件銷毀時,會自動解除訂閱
*/
this.people = _store.select('people');
}
//所有狀態(tài)的改變都會通過dispatch轉(zhuǎn)發(fā)與處理
addPerson(name){
this._store.dispatch({type: ADD_PERSON, payload: name})
}
addGuest(id){
this._store.dispatch({type: ADD_GUEST, payload: id});
}
removeGuest(id){
this._store.dispatch({type: REMOVE_GUEST, payload: id});
}
removePerson(id){
this._store.dispatch({type: REMOVE_PERSON, payload: id});
}
toggleAttending(id){
this._store.dispatch({type: TOGGLE_ATTENDING, payload: id})
}
//在ngOnDestroy中不再需要手動取消訂閱
}
使用ChangeDetection.OnPush(Work Along | Completed Lesson) 在Angular中使利用中央狀態(tài)樹,可以帶來可預(yù)測性和可維護(hù)性,同時還有提升性能。為了體現(xiàn)這一性能優(yōu)勢,我們可以使用ChangeDetection.OnPush。 OnPush背景的概念很簡單,當(dāng)組件僅依賴于輸入,而這些輸入的引用沒有發(fā)生改變,Angular就可以跳過組件的變化檢測。如前所述,所有state的委托應(yīng)該在smart component或頂級組件中處理。這使我們的應(yīng)用程序大多數(shù)組件完全依賴于輸入,安全地允許我們在組件 中將ChangeDetectionStrategy設(shè)置為OnPush。組件可以跳過組件的變化檢測,直到必要時,它將會給我們免費(fèi)提升性能。 使用OnPush更改組件的變化檢測,我們需要在@Component裝飾器中修改changeDetection屬性設(shè)置為ChangeDetection.OnPUsh。,如此,現(xiàn)在Angular將會跳過這些組件和子組件的變化檢測,直到輸入的引用改變。 更新為ChangeDetection.OnPush@Component({
selector: 'person-list',
template: `
<ul>
<li
*ngFor="let person of people"
[class.attending]="person.attending"
>
{{person.name}} - Guests: {{person.guests}}
<button (click)="addGuest.emit(person.id)">+</button>
<button *ngIf="person.guests" (click)="removeGuest.emit(person.id)">-</button>
Attending?
<input type="checkbox" [(ngModel)]="person.attending" (change)="toggleAttending.emit(person.id)" />
<button (click)="removePerson.emit(person.id)">Delete</button>
</li>
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
/*
使用OnPush改變變化檢測,使得組件僅依賴于輸入引用的變化 ,這將大大提升應(yīng)用程序的性能。
*/
export class PersonList {
/*
"dumb"組件基于輸入顯示數(shù)據(jù)和發(fā)射相關(guān)的事件給父組件或"container"組件處理,除此之外沒有別的。
*/
@Input() people;
@Output() addGuest = new EventEmitter();
@Output() removeGuest = new EventEmitter();
@Output() removePerson = new EventEmitter();
@Output() toggleAttending = new EventEmitter();
}
State擴(kuò)展(Work Along | Completed Lesson) 多數(shù)Store應(yīng)用程序都是由多個reducer組成的,每個reducer都可以管理自己的狀態(tài)。對于這個示例,我們有兩個reducer,一個用于管理part attendees,另一個應(yīng)用于列表當(dāng)前 action的過濾器。我們首先定義action的常量,指定用戶應(yīng)用可以應(yīng)用的過濾器。 我們先創(chuàng)建partFilter的reducer。對于此我們有幾個選項(xiàng)。首先是簡的返回filter的過濾器字符串。然后,我們可以編寫一個基于當(dāng)前action的過濾器過濾列表的服務(wù)或組件中的方法。雖然這個做還不錯,但是根據(jù)當(dāng)前的過濾器狀態(tài)返回應(yīng)用于part list的功能是更可擴(kuò)展的。在將來,添加更多的過濾器就像創(chuàng)建一個新的case語句一樣適當(dāng)簡單的返回對應(yīng)的投影處理函數(shù)。 Part Filter Reducerimport {
SHOW_ATTENDING,
SHOW_ALL,
SHOW_WITH_GUESTS
} from './actions';
//根據(jù)所選過濾器然后返回對應(yīng)的結(jié)果
export const partyFilter = (state = person => person, action) => {
switch(action.type){
case SHOW_ATTENDING:
return person => person.attending;
case SHOW_ALL:
return person => person;
case SHOW_WITH_GUESTS:
return person => person.guests;
default:
return state;
}
};
Part Filter Actions//Party Filter常量 export const SHOW_ATTENDING = 'SHOW_ATTENDING'; export const SHOW_ALL = 'SHOW_ALL'; export const SHOW_WITH_GUESTS = 'SHOW_GUESTS'; Party FIlter Selectimport {Component, Output, EventEmitter} from "angular2/core";
import {
SHOW_ATTENDING,
SHOW_ALL,
SHOW_WITH_GUESTS
} from './actions';
@Component({
selector: 'filter-select',
template: `
<div class="margin-bottom-10">
<select #selectList (change)="updateFilter.emit(selectList.value)">
<option *ngFor="let filter of filters" value="{{filter.action}}">
{{filter.friendly}}
</option>
</select>
</div>
`
})
export class FilterSelect {
public filters = [
{friendly: "All", action: SHOW_ALL},
{friendly: "Attending", action: SHOW_ATTENDING},
{friendly: "Attending w/ Guests", action: SHOW_WITH_GUESTS}
];
@Output() updateFilter : EventEmitter<string> = new EventEmitter<string>();
}
視圖中的狀態(tài)片段(Work Along | Completed Lesson) @Component({
selector: 'app',
template: `
<h3>@ngrx/store Party Planner</h3>
<party-stats
[invited]="(people | async)?.length"
[attending]="(attending | async)?.length"
[guests]="(guests | async)"
>
</party-stats>
<filter-select
(updateFilter)="updateFilter($event)"
>
</filter-select>
<person-input
(addPerson)="addPerson($event)"
>
</person-input>
<person-list
[people]="people | async"
[filter]="filter | async"
(addGuest)="addGuest($event)"
(removeGuest)="removeGuest($event)"
(removePerson)="removePerson($event)"
(toggleAttending)="toggleAttending($event)"
>
</person-list>
`,
directives: [PersonList, PersonInput, FilterSelect, PartyStats]
})
export class App {
public people;
private subscription;
constructor(
private _store: Store
){
this.people = _store.select('people');
/*
this is a naive way to handle projecting state, we will discover a better
Rx based solution in next lesson
*/
this.filter = _store.select('partyFilter');
this.attending = this.people.map(p => p.filter(person => person.attending));
this.guests = this.people
.map(p => p.map(person => person.guests)
.reduce((acc, curr) => acc + curr, 0));
}
//...rest of component
}
@Component({
selector: 'person-list',
template: `
<ul>
<li
*ngFor="let person of people.filter(filter)"
[class.attending]="person.attending"
>
{{person.name}} - Guests: {{person.guests}}
<button (click)="addGuest.emit(person.id)">+</button>
<button *ngIf="person.guests" (click)="removeGuest.emit(person.id)">-</button>
Attending?
<input type="checkbox" [checked]="person.attending" (change)="toggleAttending.emit(person.id)" />
<button (click)="removePerson.emit(person.id)">Delete</button>
</li>
</ul>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PersonList {
@Input() people;
//for now, we will pass filter down and apply
@Input() filter;
@Output() addGuest = new EventEmitter();
@Output() removeGuest = new EventEmitter();
@Output() removePerson = new EventEmitter();
@Output() toggleAttending = new EventEmitter();
}
//timerOne emits first value at 1s, then once every 4s
const timerOne = Rx.Observable.timer(1000, 4000);
//timerTwo emits first value at 2s, then once every 4s
const timerTwo = Rx.Observable.timer(2000, 4000)
//timerThree emits first value at 3s, then once every 4s
const timerThree = Rx.Observable.timer(3000, 4000)
//when one timer emits, emit the latest values from each timer as an array
const combined = Rx.Observable
.combineLatest(
timerOne,
timerTwo,
timerThree
);
const subscribe = combined.subscribe(latestValues => {
//grab latest emitted values for timers one, two, and three
const [timerValOne, timerValTwo, timerValThree] = latestValues;
/*
Example:
timerOne first tick: 'Timer One Latest: 1, Timer Two Latest:0, Timer Three Latest: 0
timerTwo first tick: 'Timer One Latest: 1, Timer Two Latest:1, Timer Three Latest: 0
timerThree first tick: 'Timer One Latest: 1, Timer Two Latest:1, Timer Three Latest: 1
*/
console.log(
`Timer One Latest: ${timerValOne},
Timer Two Latest: ${timerValTwo},
Timer Three Latest: ${timerValThree}`
);
});
//combineLatest also takes an optional projection function
const combinedProject = Rx.Observable
.combineLatest(
timerOne,
timerTwo,
timerThree,
(one, two, three) => {
return `Timer One (Proj) Latest: ${one},
Timer Two (Proj) Latest: ${two},
Timer Three (Proj) Latest: ${three}`
}
);
//log values
const subscribe = combinedProject.subscribe(latestValuesProject => console.log(latestValuesProject));
//Create an observable that emits a value every second
const myInterval = Rx.Observable.interval(1000);
//Create an observable that emits immediately, then every 5 seconds
const myTimer = Rx.Observable.timer(0, 5000);
//Every time interval emits, also get latest from timer and add the two values
const latest = myInterval
.withLatestFrom(myTimer)
.map(([interval, timer]) => {
console.log(`Latest Interval: ${interval}`);
console.log(`Latest Timer: ${timer}`);
return interval + timer;
});
//log total
const subscribe = latest.subscribe(val => console.log(`Total: ${val}`));
@Component({
selector: 'app',
template: `
<h3>@ngrx/store Party Planner</h3>
<party-stats
[invited]="(model | async)?.total"
[attending]="(model | async)?.attending"
[guests]="(model | async)?.guests"
>
{{guests | async | json}}
</party-stats>
<filter-select
(updateFilter)="updateFilter($event)"
>
</filter-select>
<person-input
(addPerson)="addPerson($event)"
>
</person-input>
<person-list
[people]="(model | async)?.people"
(addGuest)="addGuest($event)"
(removeGuest)="removeGuest($event)"
(removePerson)="removePerson($event)"
(toggleAttending)="toggleAttending($event)"
>
</person-list>
`,
directives: [PersonList, PersonInput, FilterSelect, PartyStats]
})
export class App {
public model;
constructor(
private _store: Store
){
/*
Every time people or partyFilter emits, pass the latest
value from each into supplied function. We can then calculate
and output statistics.
*/
this.model = Observable.combineLatest(
_store.select('people')
_store.select('partyFilter'),
(people, filter) => {
return {
total: people.length,
people: people.filter(filter),
attending: people.filter(person => person.attending).length,
guests: people.reduce((acc, curr) => acc + curr.guests, 0)
}
});
}
//...rest of component
}
export const partyModel = () => {
return state => state
.map(([people, filter]) => {
return {
total: people.length,
people: people.filter(filter),
attending: people.filter(person => person.attending).length,
guests: people.reduce((acc, curr) => acc + curr.guests, 0)
}
});
};
export const attendees = () => {
return state => state
.map(s => s.people)
.distinctUntilChanged();
};
export const percentAttending = () => {
return state => state
//build on previous selectors
.let(attendees())
.map(p => {
const totalAttending = p.filter(person => person.attending).length;
const total = p.length;
return total > 0 ? (totalAttending / total) * 100 : 0;
});
};
export class App {
public model;
constructor(
private _store: Store
){
/*
Every time people or partyFilter emits, pass the latest
value from each into supplied function. We can then calculate
and output statistics.
*/
this.model = Observable.combineLatest(
_store.select('people'),
_store.select('partyFilter')
)
//extracting party model to selector
.let(partyModel());
//for demonstration on combining selectors
this.percentAttendance = _store.let(percentAttending());
}
//...rest of component
}
interface Selector<T,V> {
(state: Observable<T>): Observable<V>
}
//pre middleware takes an observable of actions, returning an observable
export const actionLogger = action => {
return action.do(a => console.log('DISPATCHED ACTION:', a));
}
//post middleware takes an observable of state, returning observable
export const stateLogger = state => {
return state.do(s => console.log('NEW STATE:', s));
}
bootstrap(App, [
provideStore({people, partyFilter}),
usePreMiddleware(actionLogger),
usePostMiddleware(stateLogger)
]);
import {Injectable} from 'angular2/core';
//simple service wrapping local storage
@Injectable()
export class LocalStorageService {
setItem(key, value){
localStorage.setItem(key, JSON.stringify(value));
}
getItem(key){
return JSON.parse(localStorage.getItem(key));
}
}
/*
create middleware with a dependency on the localStorageService
basic example, accept state key to sync with local storage
*/
export const localStorageMiddleware = key => createMiddleware(localStorageService => {
return state => {
//sync specified state slice with local storage
return state.do(state => localStorageService.setItem(key, state[key]));
}
}, [LocalStorageService]);
import {provide, Provider} from 'angular2/core';
import {INITIAL_STATE} from '@ngrx/store';
export const rehydrateState = key => {
//override initial state token for use in store
return provide(INITIAL_STATE, {
useValue: { [key]: JSON.parse(localStorage.getItem(key))};
});
};
bootstrap(App, [
LocalStorageService,
provideStore({people, partyFilter}),
usePreMiddleware(actionLogger),
usePostMiddleware(stateLogger, localStorageMiddleware('people')),
rehydrateState('people')
]);
export const RESET_STATE = 'RESET_STATE';
const INIT = '__NOT_A_REAL_ACTION__';
export const reset = reducer => {
let initialState = reducer(undefined, {type: INIT})
return function (state, action) {
//if reset action is fired, return initial state
if(action.type === RESET_STATE){
return initialState;
}
//calculate next state based on action
let nextState = reducer(state, action);
//return nextState as normal when not reset action
return nextState;
}
}
bootstrap(App, [
//wrap people in reset meta-reducer
provideStore({people: reset(people), partyFilter})
]);
export function combineReducers(reducers: any): Reducer<any> {
const reducerKeys = Object.keys(reducers);
const finalReducers = {};
for (let i = 0; i < reducerKeys.length; i++) {
const key = reducerKeys[i];
if (typeof reducers[key] === 'function') {
finalReducers[key] = reducers[key];
}
}
const finalReducerKeys = Object.keys(finalReducers);
return function combination(state = {}, action) {
let hasChanged = false;
const nextState = {};
for (let i = 0; i < finalReducerKeys.length; i++) {
const key = finalReducerKeys[i];
const reducer = finalReducers[key];
const previousStateForKey = state[key];
const nextStateForKey = reducer(previousStateForKey, action);
nextState[key] = nextStateForKey;
hasChanged = hasChanged || nextStateForKey !== previousStateForKey;
}
return hasChanged ? nextState : state;
};
}
天之驕子 2017.8.20 深圳 |
|
|