像傳統(tǒng)數(shù)據(jù)一樣,代表著應(yīng)用的記錄點(diǎn),你的Store可以被認(rèn)為是客戶端"真實(shí)的數(shù)據(jù)來源" 或 數(shù)據(jù)庫。在設(shè)計(jì)應(yīng)用時(shí)都遵守一個(gè)Store的約定,Store任何時(shí)刻的存儲快照都將只是呈現(xiàn)應(yīng)用程序的完整狀態(tài)。 一個(gè)單一的、不可變的狀樹,只有通過顯式定義和調(diào)度才能更新。 中心化,不可變狀態(tài) ReducersStore應(yīng)用程序的第二個(gè)組成部分是reducers。A2 reducer 是一個(gè) a3純函數(shù),前一個(gè)狀態(tài)和一個(gè)與事件相關(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ù)的返回值類型由其輸入值的類型決定的。 一個(gè)簡單的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)需要更新時(shí),我們?nèi)绾闻creducers通信呢?這是actions4,在Store應(yīng)用程序中,所有導(dǎo)致狀態(tài)更新的用戶交互都必須以actions的形式表示。所有與用戶相關(guān)的事件都被分派為action,經(jīng)過Store的4個(gè)action通道,然后輸出一個(gè)新的狀態(tài)表示。每次調(diào)度一個(gè)action時(shí)都會發(fā)生這個(gè)過程,留下應(yīng)用程序狀態(tài)隨時(shí)間變化的完整的,可序列話的表示形式。 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'); //合并多個(gè)state Observable.combineLatest( store.select('people'), store.select('events'), (people, events) => { // 在此投影 } ) Not Your Classic Angular在上一節(jié)中,我提到了在開發(fā)應(yīng)用程序時(shí)遵守的約定。在傳統(tǒng)的設(shè)置和工作流程,你已經(jīng)習(xí)慣了嗎,這是什么意思?讓我們一起來看看。 如果你是從Angular1轉(zhuǎn)過來的,你會很熟悉數(shù)據(jù)的雙向綁定6??刂破靼裮odel綁定到視圖,反之亦然。這種方法的問題出現(xiàn)在你的視圖變得更新復(fù)雜時(shí),需要控制器和指令來管理并表示重要的狀態(tài)隨時(shí)間的變化。這很快變成一個(gè)噩夢,無論是推理還是調(diào)度,因?yàn)橐粋€(gè)變化會影響另一個(gè)變化,另一個(gè)又影響另一個(gè)......等等。 Store提升了單向數(shù)據(jù)流7和顯式調(diào)度操作的概念。的感受狀態(tài)更新都緩存在組件中,委托給reducer。在應(yīng)用程序中啟動狀態(tài)更新的唯一辦法是通過調(diào)度操作,對應(yīng)于特定的reducer案例。這不僅使你應(yīng)用程序的狀態(tài)改變變得簡單,因?yàn)楦率羌显谝黄鸬?,它會在出現(xiàn)錯(cuò)誤時(shí)留下清晰的線索。 雙向數(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)勢在整個(gè)概述中,我們簡要介紹了利用Store在一種典型的Angular 1風(fēng)格方法的優(yōu)勢,現(xiàn)在讓我們發(fā)一點(diǎn)時(shí)間來回顧一下。為什么要花時(shí)間在這個(gè)特定的庫,構(gòu)建和曲線上投資呢?Store的優(yōu)勢是狀態(tài)中心化,性能,測試。 中心化,狀態(tài)不可變所有相關(guān)應(yīng)用程序的狀態(tài)都緩存在一個(gè)位置。這樣可以很容易地跟蹤問題,因?yàn)殄e(cuò)誤時(shí)的狀態(tài)快照可以提供重要的見解,并且可以輕松的重新重現(xiàn)這個(gè)問題。這也使得眾多困難問題,例如在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ù)雜且容易出錯(cuò)。 工具與生態(tài)系統(tǒng)中心化的,不可變的狀態(tài)還可以實(shí)現(xiàn)更強(qiáng)大的工具。一個(gè)這樣的盒子是ngrx開發(fā)工具,它提供了action和狀態(tài)變化的歷史,允許在開發(fā)過程中進(jìn)行8次遍歷。Store提供的模式還允許一個(gè)易于實(shí)現(xiàn)中間件的豐富的生態(tài)系統(tǒng)。因?yàn)镾tore在分派action之前和之后都提供一個(gè)入口點(diǎn),所以應(yīng)用程序減少,如同步片狀態(tài)到本地Store,高級日志記錄和實(shí)現(xiàn)sagas這樣的問題可以通過快速包和幾行代理來解決。這個(gè)生態(tài)系統(tǒng)只會在未來幾個(gè)月內(nèi)增長。 操作調(diào)度action和狀態(tài)更改的歷史 ,以模擬應(yīng)用程序交互的時(shí)間點(diǎn)。 @ngrx/store的構(gòu)建模塊在構(gòu)建Store應(yīng)用程序之前,首先來看看構(gòu)建@ngrx/store的RxJS概念。首先理解這些概念,我們將來可以更有效地利用這個(gè)庫。要詳細(xì)說明下面的每個(gè)主題,請查看這些額外的資源。 聲明:Mike Ryan和Rob Wormald的實(shí)際@ngrx/store代碼顯著更強(qiáng)大。這些示例旨在演示涉及的RxJS概念,并從庫中移出"magic" Subject/Dispatch的探索Rx的信使們,你告訴我,我會告訴他們的..... (演示) @ngrx/store的兩個(gè)支柱,Store和Dispatcher都擴(kuò)展了RxJS主題。主題即是觀察者(Observables)和觀察者(Observers),這意味著你可以訂閱Subject,但也可以將主題訂閱源。在高級別科目可以被認(rèn)為是信使或代理人。 因?yàn)镾ubject是Observables,你可以 "next" 或直接將值傳遞到流中。然后,該Subject的訂閱將被通知發(fā)出值。在Store的上下文中,這些用戶可能是一個(gè)Angular 服務(wù), 組件或需要訪問應(yīng)用程序狀態(tài)的任何內(nèi)容。 訂閱主題//創(chuàng)建一個(gè)主題 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)的下一個(gè)方法。這被用于將值發(fā)送到Subject中,然后將這些值發(fā)送給子對象。 將Dispatcher繼承自Subject/* redux/ngrx-store 有一個(gè)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)建一個(gè)dispatcher(只是一個(gè)包含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完美地工作,但它們有一個(gè)問題可以防止他們適合Store。訂閱Subject時(shí),只接收訂閱后發(fā)出的值。在不斷添加和刪除組件的環(huán)境中,這是不可接受的,在訂閱時(shí)需要應(yīng)用程序Store的最新的按需狀態(tài)部分。 Subjects只接受訂閱后發(fā)出的值 /* 現(xiàn)在我們有一個(gè)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!'); /* 我們在添加一個(gè)訂閱者。 由于我們第一次實(shí)施Store是一個(gè)subject,訂閱者只能看到發(fā)布的價(jià)值*AFTER*他們訂閱之后。在這種情況下,訂閱者3將不了解'FIRST VALUE!' */ const subscriberThree = myFirstStore.suscribe(val => { console.log('***SUBSCRIBER THREE***', val); }); 幸運(yùn)的是,RxJS為Subject處理這個(gè)問題提供了BehaviorSubject。 即BehviorSubject 封裝了Subject的所有功能,但也可以在訂閱后將改后發(fā)布的值返回給訂閱都。這意味著組件和服務(wù)將始終可以訪問最新(或初始值)應(yīng)用程序狀態(tài)和所有將來的更新。 BehaviorSubject訂閱接收上一次發(fā)布的值/* 因?yàn)槲覀兊慕M件需要查詢當(dāng)前狀態(tài),所以BehaviorSubject更適合Store。BehaviorSubjects具有Subject的所有功能,還允許設(shè)置初始值,以及在訂閱時(shí)將所接收的最后一個(gè)值輸出給所有觀察者。 */ 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ā)布之后添加另一個(gè)訂閱者 //輸出:***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。 這個(gè)流水線的創(chuàng)建是在創(chuàng)建時(shí)dispatch傳遞給store處理的。然后,store下一個(gè)方法被覆蓋,以便將新的狀態(tài)表示傳遞到store之前,首先將所有的action都dispatch管道。這也允許通過dispatch匯集接收到的action。 現(xiàn)在,中間件和reducers的實(shí)現(xiàn)將被刪除。 將Dispatcher與Store關(guān)聯(lián)一起/* 所有action都應(yīng)通過管道,然后新計(jì)算的狀態(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)管道,計(jì)算新狀態(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下一個(gè)方法允許我們將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的組合是在任何給定時(shí)間組成應(yīng)用程序狀態(tài)的表示。 在討論如何創(chuàng)建和實(shí)現(xiàn)reducers之前 , 我們先來看看reduce函數(shù)。reduce需要一個(gè)數(shù)組,根據(jù)累加值和當(dāng)前值運(yùn)行一個(gè)函數(shù),在完成后將數(shù)組遞減一個(gè)值。你可以把reducers看成一個(gè)滾雪而下的雪橇,每一次變革都會變得很大。以相同的方式,減少reduce是通過迭代定義的函數(shù)應(yīng)用于當(dāng)前值的結(jié)果。 標(biāo)準(zhǔn)的Reduce/* 你可以想一下滾雪球的場景。每一次翻滾都會累加質(zhì)量和體積直到到達(dá)底部。reduce也類似,返回的值傳遞給所有提供函數(shù)的下一個(gè)調(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接受一個(gè)state和action作為參數(shù),暴露一個(gè)switch語句(一般來說,盡管 這可以通過多種方式處理)定義reducer所涉及的action類型。每次分派一個(gè)action時(shí),將調(diào)用注冊到store的每個(gè)reducer(通過根reducer, 在應(yīng)用程序引導(dǎo)時(shí)在provideStore中創(chuàng)建),傳遞該狀態(tài)片段(累加器)的當(dāng)前狀態(tài)和已分派的action。如果reducer沒有被注冊來處理該action類型,則將執(zhí)行適當(dāng)?shù)臓顟B(tài)計(jì)算和狀態(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; }, {}); }; /* 這讓我們大部的方式在那里,但真正希望我們想要的是第一個(gè)和第二個(gè)的值累加隨著action隨著時(shí)間推移。幸運(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,但值隨著時(shí)間的推移累加。。。 scan操作符以類似的方式扮作reduce,除了累加器隨時(shí)間保持,或直接scan應(yīng)用的可觀察完成。例如,當(dāng)分派action和新的狀態(tài)輸出時(shí),scan函數(shù) 中的累加器將始終是狀態(tài)的最后一個(gè)輸出表示形式。這減輕了需要維護(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示例隨著時(shí)間的推移建立對象 const objectScan = testSubjectTwo.scan((acc, curr) => Object.assign({}, acc,curr), {}); // 記錄累加值 const subscribe = objectScan.subscribe(val => console.log('Accumulated object:', val)); //傳遞值到testSubject,添加屬性到一個(gè)新對象 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比喻成一個(gè)雪球,越滾越大(或在原來的基礎(chǔ)上累加)。scan也是類似行為。累加器(在這個(gè)示例中是,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管理中間件讓我擁有整個(gè)可觀察的。。。 (let demo | store demo) 中間件已經(jīng)在ngrx/store v2中移出了。通這個(gè)部分來閱讀本書,以了解let操作符,因?yàn)樗梢耘c選擇器一起使用。 雖然大多數(shù)運(yùn)算符都是從可觀察的值傳遞出來的,但是我們可以把整個(gè)可觀察的數(shù)據(jù)傳遞出去在返回源可觀察數(shù)據(jù)之前,這允許有機(jī)會處理額外的操作符和功能。雖然這可能看起來像一個(gè)小小的細(xì)微差別,但它完全適合于中間件或選擇器(稍后討論)的情況,消費(fèi)者想要定義一個(gè)可利用的,可重復(fù)使用的代碼塊,以插入到可觀察鏈中的特定時(shí)隙。 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 操作符擁有整個(gè)observable .let(obs => obs.map(val => val +2)) .subscribe(val => console.log('VALUE FROM ARRAY WITH let :', val)); //let 操作符提供靈活度,添加多個(gè)操作符到源observable然后返回 const letTestThree = myObservableArray .map(val => val +1) //let 操作符擁有整個(gè)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)之前 或之后添加自定義功能需要一個(gè)入口點(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 操作符接受整個(gè)源observable,返回一個(gè)新的observable //@ngrx/store 組成中間件,所以你可以提供多個(gè)功能 //在我們下面的示例中,會接受一個(gè)前件和一個(gè)后件 //中間件標(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)的鍵/值對象映射,所以提供一個(gè)輔助函數(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)片段 //這個(gè)一種簡單的輔助功能,使得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)用程序中的每個(gè)視圖都只關(guān)心自己的狀態(tài)片段。由于性能原因,我們不希望從所選狀片段中發(fā)出新值,除非已進(jìn)行更新。幸運(yùn)的是,對于我們來說,RxJS有一個(gè)很好用的操作符做這事(注意趨勢)。distinctUntilChanged操作符將僅在下一個(gè)值為唯一時(shí)基于先前發(fā)出的值發(fā)出。在數(shù)字和字符串的情況下,這意味著相等的數(shù)字和字符串,在對象的情況下,如果 對象引用是相同的新對象將不會被發(fā)出。 在基本類型與引用類型上使用distinctUntilChanged//只會輸出唯一的值,同時(shí)是基于最新發(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]); //只輸出唯一的對象,同時(shí)是基于最新發(fā)出的值 const nonDistinctObjects = myArrayWithDuplicateObjects .distinctUntilChanged() //output: 'DISTINCT OBJECTS: {name: 'Test'} .subscribe(val => console.log('DISTINCT OBJECTS:',val)); 回想一下,store的reducers始終具有默認(rèn)的情況,如果與分派的action不相關(guān),則返回上一個(gè)狀態(tài)。這意味著,在應(yīng)用程序中選擇狀態(tài)片段時(shí),除非更新了特定的片段,否則不會接收更新。這有助于使你的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僅在輸出不同時(shí)才發(fā)出新值,最后發(fā)出的值。在下面的示例中,distinctUntilChanged操作符的可觀察值將發(fā)送一個(gè)較小的值,而另一個(gè)僅使用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' }); 演練一個(gè)簡單的應(yīng)用程序我們將要?jiǎng)?chuàng)建的示例程序10是一個(gè)簡單入門的應(yīng)用。用戶應(yīng)該能夠輸入?yún)⒓诱呒捌淇腿说牧斜?,跟蹤認(rèn)證確認(rèn)出席者,通過特定標(biāo)準(zhǔn)過濾與會者,并快速查看有關(guān)活動的重要統(tǒng)計(jì)信息。在整個(gè)應(yīng)用程序的創(chuàng)建過程中,我們將探討@ngrx/stre的核心概念,并討論流行的模式和最佳做法。 在每個(gè)部分上方提供了兩個(gè)鏈接 ,工作和完成課程。如果 你希望按照概念進(jìn)行編碼,則每個(gè)課程的開始時(shí),'工作'鏈接 將取提取。否則,"完成課程"鏈接 允許你從當(dāng)前課程的終點(diǎn)開始。 不多說了,我們開始吧。 Party Planner應(yīng)用程序結(jié)構(gòu)圖10設(shè)置第一個(gè)Reducer(Work Along | Completed Lesson) Reducer是你應(yīng)用程序的基礎(chǔ)。隨著應(yīng)用程序緩存維護(hù)狀態(tài),reducer是動態(tài)調(diào)度時(shí)action和輸出新狀態(tài)表示的主要內(nèi)容。每個(gè)reducer應(yīng)該集中在與數(shù)據(jù)庫的表類似的特定部分或狀態(tài)片段上。 創(chuàng)建reducer是很簡單的一旦習(xí)慣了一個(gè)常見的成語,從不改變以前的狀態(tài),并總是返回一個(gè)新的狀態(tài)表示,當(dāng)相關(guān)的action被調(diào)度。如果 你是新來的store或Redux模式,這需要一些習(xí)慣來感覺自然。而不是使用諸如推送或重新分配先前存在的對象的變量的方法,而是依靠沒有改變的方法,如concat和Object.assign來返回新值。還困惑嗎?讓我們來看看個(gè)人在實(shí)踐中與其他的reducer有什么關(guān)系。 person的reducer需要處理5個(gè)action,移出一個(gè)person,向guest添加一個(gè)guest,從一個(gè)person中移出guest,并切換他們是否參加活動。為此, 我們將創(chuàng)建一個(gè)reducer函數(shù) ,接受以前的狀態(tài)和當(dāng)前 調(diào)度的action。然后,我們需要實(shí)現(xiàn)一個(gè)case語句,在執(zhí)行相關(guān)action時(shí)執(zhí)行正常的狀態(tài)重新計(jì)算。 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時(shí),始終會返回先前狀態(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)化你的操作,同時(shí)提供額外的類型安全性,被稱之為action的創(chuàng)建者。 對于我們的就應(yīng)用程序,我們將每個(gè)應(yīng)用程序action導(dǎo)出字符 串常量。然后,這些將用于我們r(jià)educer案例語句的關(guān)鍵字以及每個(gè)派生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ū)別哪個(gè)一個(gè)是start component,哪一個(gè)是dumb component? Smart 或Container 組件應(yīng)該作為你的首層組件,或路由組件 。這些組件通??梢灾苯釉L問store或?qū)С鼋M件。Smart Component組件通過服務(wù)或直接處理視圖event和action的dispatch。同時(shí),Smart Component組件還處理在同一視圖中從子組件發(fā)出的事件邏輯處理。 Dumb或Child組件通常僅作為顯示內(nèi)容,僅依賴于@Input參數(shù) ,以及合理的方式處理接收到的數(shù)據(jù) 。當(dāng)相關(guān)事件性在dump components時(shí),它們通過emit操作發(fā)射出來,交由smart父組件處理。你應(yīng)用程序中將大部分由dump組件構(gòu)成,因?yàn)樗麄兪仟?dú)立的,聚焦的,并且是可重復(fù)利用的。 part planner應(yīng)用程序需要一個(gè)container component。該組件將負(fù)責(zé)將適當(dāng)?shù)臓顟B(tài)轉(zhuǎn)交給每個(gè)子組件,并根據(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是一個(gè)獨(dú)特的,狀態(tài)化的管道,可以處理Observables和Promises。當(dāng)在具有Observables的模板表達(dá)式中使用AsyncPipe管道時(shí),使用了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中手動取消訂閱。現(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)組件銷毀時(shí),會自動解除訂閱 */ 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ù)性,同時(shí)還有提升性能。為了體現(xiàn)這一性能優(yōu)勢,我們可以使用ChangeDetection.OnPush。 OnPush背景的概念很簡單,當(dāng)組件僅依賴于輸入,而這些輸入的引用沒有發(fā)生改變,Angular就可以跳過組件的變化檢測。如前所述,所有state的委托應(yīng)該在smart component或頂級組件中處理。這使我們的應(yīng)用程序大多數(shù)組件完全依賴于輸入,安全地允許我們在組件 中將ChangeDetectionStrategy設(shè)置為OnPush。組件可以跳過組件的變化檢測,直到必要時(shí),它將會給我們免費(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)用程序都是由多個(gè)reducer組成的,每個(gè)reducer都可以管理自己的狀態(tài)。對于這個(gè)示例,我們有兩個(gè)reducer,一個(gè)用于管理part attendees,另一個(gè)應(yīng)用于列表當(dāng)前 action的過濾器。我們首先定義action的常量,指定用戶應(yīng)用可以應(yīng)用的過濾器。 我們先創(chuàng)建partFilter的reducer。對于此我們有幾個(gè)選項(xiàng)。首先是簡的返回filter的過濾器字符串。然后,我們可以編寫一個(gè)基于當(dāng)前action的過濾器過濾列表的服務(wù)或組件中的方法。雖然這個(gè)做還不錯(cuò),但是根據(jù)當(dāng)前的過濾器狀態(tài)返回應(yīng)用于part list的功能是更可擴(kuò)展的。在將來,添加更多的過濾器就像創(chuàng)建一個(gè)新的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 深圳 |
|