Redux 提供的主要功能:全局數據管理,包括數據的更新、存儲、數據變化通知。 Redux 的 store 中存放了當前應用的狀態,能夠根據這個狀態完整恢復出當前應用的界面,所以在使用 Redux 的項目中,能夠實現一個比較炫酷的功能:依據狀態的前進、後退。react
Redux 中主要有三大塊:redux
Action:指代引發 Redux 中數據變化的操做;數據結構
Reducer:響應 Action 操做,修改 Redux 中的數據;架構
Store:包含一個 state 對象,用於存放整個應用的數據,並整合將 Action 和 Reducer 整合起來,修改 Store 中的數據。函數
目前,網上已經有不少中文資料介紹具體概念細節以及相關 API 了,好比:this
Redux 中文文檔spa
這裏主要想記錄一下做爲一個初學 Redux 的菜鳥,使用過程當中的心得體會。設計
單單就 Redux 自己來看,並不能直接用於生產,太靈活了,有不少「套路」須要強制定下來:code
怎麼設計 state 對象的數據結構?怎麼分好模塊?怎麼規定各個模塊的命名風格?
Redux 只提供了註冊 state 變化回調函數的 API ,若是隻想監聽其中某一個數據的變化該怎麼辦?
若是在 state change 的回調函數中再次 dispatch Action ,就可能形成無限遞歸,怎麼設計才能很好地避免這種無限遞歸?
如何設計組織項目代碼才更好維護?
如何避免寫大量重複的 Action 、 Reducer 代碼?
目錄的劃分方式有多種,能夠按照項目的功能模塊,也能夠按照 Redux 的職責模塊。我選擇了後者,採用的目錄結構以下:
外部能夠直接引入的 JS 模塊只能是 data/main
和 data/actionTypes
。
爲何會有遞歸調用呢?參考以下代碼:
import {createStore} from 'redux'; function reducer(state = {}, action) { switch (action.type) { case 'SOME_THING_LOAD_COMPLETE': return Object.assign({}, state, { loadComplete: true }); default: return state; } } let store = createStore(reducer); store.subscribe(() => // do something here. store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'}); ); store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'});
上面這個簡單的例子很清晰地說明了無限遞歸的問題,在實際開發中,因爲業務邏輯的複雜糾纏,這個遞歸過程可能很是間接、隱蔽,形成 debug 困難。那麼如何有效避免呢?
一個比較經常使用的方法就是檢查對應數據是否真的發生了變化,好比上面的代碼能夠改成:
import {createStore} from 'redux'; function reducer(state = {}, action) { switch (action.type) { case 'SOME_THING_LOAD_COMPLETE': return Object.assign({}, state, { loadComplete: true }); default: return state; } } let store = createStore(reducer); let previousLoadComplete; store.subscribe(() => // do something here. let currentLoadComplete = store.getState().loadComplete; if (currentLoadComplete !== previousLoadComplete) { store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'}); previousLoadComplete = currentLoadComplete; } ); store.dispatch({type: 'SOME_THING_LOAD_COMPLETE'});
因爲 state 每次更新都會在相應位置產生一個新對象,因此只須要用全等來判斷就好了。
如何劃分 state 對象的結構呢?可能每一個人根據本身的經驗,都有本身的一套劃分方式。此處我採用了與業務功能模塊對齊的原則。
好比,個人項目裏面有這樣一些頁面:用戶列表頁面、用戶詳情頁面、資源頁面、資源詳情頁面,那麼 state 對象的結構爲:
state = { 'user.list': { ... }, 'user.detail': { ... }, 'resource.list': { ... }, 'resource.detail': { ... } }
結構扁平化了。
我的建議,不要使用「多層」的 state 結構,好比把上面的例子設計成:
// BAD state = { user: { list: { ... }, detail: { ... } }, resource: { list: { ... }, detail: { ... } } };
過深的結構會帶來沒必要要的複雜度。
Redux 只提供了 subscribe
方法來監聽 state 的變化,在實際開發中,某一個組件可能只對某部分 state 變化感興趣。因此,應當適當地作一下擴展:
export default class StateWatcher { /** * 存放監聽回調函數等 * * @private * @type {Array.<Object>} */ watcherList = []; /** * 構造函數 * * @constructor * @param {store} Store Redux store實例 */ constructor(store) { this.store = store; // 存放以前 state this.previousStoreState = extend({}, this.store.getState()); this.subscribe(); } /** * 訂閱 store 中 state 變化事件,會去檢查 watcherList 中是否存在相應數據變化的回調函數 * * @private */ subscribe() { let me = this; this.unsubscribe = this.store.subscribe(function () { let currentStoreState = me.store.getState(); let changedPropertyNameList = []; let delayFns = []; // 遍歷 watcherList ,查找註冊的回調函數 each(me.watcherList, watcher => { let propertyName = watcher.propertyName; let previous = me.previousStoreState[propertyName]; if (currentStoreState[propertyName] !== previous) { changedPropertyNameList.push(propertyName); // 這裏 context 對應的是某個組件,若是組件銷燬了,就沒有必要調用相應回調函數了。 if (!watcher.context.isInStage(componentState.DESTROIED)) { // 回調函數得延遲執行,由於回調函數是不可控的,在回調函數中可能又 dispatch 另外的 action , // 那就至關於這次 action 還沒處理完,新的又來了,容易形成莫名其妙的錯誤。 // 因此要秉承處理完當前 action 的前提下,才能處理下個 action 的原則。 delayFns.push(() => { watcher.watcherFn.call( watcher.context, propertyName, currentStoreState[propertyName], previous ); }); } } }); // 統一更新屬性 each(changedPropertyNameList, propertyName => { me.previousStoreState[propertyName] = currentStoreState[propertyName]; }); // action 處理完以後,統一調用延遲函數。 each(delayFns, fn => fn()); }); } /** * 添加屬性變化的回調函數 * * @public * @param {string} propertyName 屬性名 * @param {Function} watcherFn 回調函數 * @param {Component} context 組件 */ addWatcher({propertyName, watcherFn, context}) { this.watcherList.push({propertyName, watcherFn, context}); } /** * 移除屬性變化的回調函數 * * @public * @param {string} propertyName 屬性名 * @param {Function} watcherFn 回調函數 * @param {Component} context 組件 */ removeWatcher({propertyName, watcherFn, context}) { this.watcherList = filter(this.watcherList, watcher => { return watcher.propertyName !== propertyName || watcher.watcherFn !== watcherFn || watcher.context !== context; }); } /** * 銷燬 * * @public */ destroy() { this.unsubscribe(); } }
目前想到的,就只是抽離複用代碼,造成 helper 方法之類的。
本文所述方案僅供參考,算是我初次使用 Redux 所想到的一些「套路」,不對之處靜候讀者指出,共同探討。