Redux 使用初探

Redux 提供的主要功能:全局數據管理,包括數據的更新、存儲、數據變化通知。 Redux 的 store 中存放了當前應用的狀態,能夠根據這個狀態完整恢復出當前應用的界面,所以在使用 Redux 的項目中,能夠實現一個比較炫酷的功能:依據狀態的前進、後退。react

Redux 中主要有三大塊:redux

  • Action:指代引發 Redux 中數據變化的操做;數據結構

  • Reducer:響應 Action 操做,修改 Redux 中的數據;架構

  • Store:包含一個 state 對象,用於存放整個應用的數據,並整合將 Action 和 Reducer 整合起來,修改 Store 中的數據。函數

目前,網上已經有不少中文資料介紹具體概念細節以及相關 API 了,好比:this

這裏主要想記錄一下做爲一個初學 Redux 的菜鳥,使用過程當中的心得體會。設計

單單就 Redux 自己來看,並不能直接用於生產,太靈活了,有不少「套路」須要強制定下來:code

  • 怎麼設計 state 對象的數據結構?怎麼分好模塊?怎麼規定各個模塊的命名風格?

  • Redux 只提供了註冊 state 變化回調函數的 API ,若是隻想監聽其中某一個數據的變化該怎麼辦?

  • 若是在 state change 的回調函數中再次 dispatch Action ,就可能形成無限遞歸,怎麼設計才能很好地避免這種無限遞歸?

  • 如何設計組織項目代碼才更好維護?

  • 如何避免寫大量重複的 Action 、 Reducer 代碼?

劃分代碼目錄

目錄的劃分方式有多種,能夠按照項目的功能模塊,也能夠按照 Redux 的職責模塊。我選擇了後者,採用的目錄結構以下:

外部能夠直接引入的 JS 模塊只能是 data/maindata/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 對象的結構爲:

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 所想到的一些「套路」,不對之處靜候讀者指出,共同探討。

相關文章
相關標籤/搜索