Redux 基礎與實踐

網易考拉前端團隊發表於2017-09-07

之前寫過一篇 Regular 元件開發的一些建議 的文章提到了日常開發Regular元件的一些槽點,並提出了在簡單需求,不使用狀態管理框架時的一些替代方案。本文的目的便是填前文的一個坑,即較複雜需求下 Redux 引入方案。

關於 Redux

Redux 是 JavaScript 狀態容器,提供可預測化的狀態管理。

看一個簡單的 DEMO

const store = redux.createStore(function(prevState, action) {
    if (!prevState) {
        prevState = {
            count: 0
        };
    }

    switch(action.type) {
        case 'REQUEST':
            return {
                count: prevState.count + 1
            }
    }

    return prevState;
});

store.dispatch({
    type: 'REQUEST'
});

store.subscribe(function () {
    console.log(store.getState());
});
複製程式碼

理解他,我們可以結合後端 MVC 的 web 模型
web架構
redux架構

演員表

Store 飾演 應用伺服器

const store = redux.createStore(f);
複製程式碼

createStore 這個 API 會建立一臺應用伺服器,包含資料庫儲存,以及一個 web Server

State 飾演 DataBase

const state = store.getState()
複製程式碼

getState 操作會返回當前的伺服器資料庫資料,這臺資料庫 bind 了 0.0.0.0(閉包),所以外界無法操作關於資料庫的資訊,只能通過內部的服務對資料庫做修改

Action 飾演請求

store.dispatch({
    type: 'type',
    payload: {}
})
複製程式碼

dispatch(action) 的操作就像是往伺服器傳送請求。action.type 就像是請求的 uri,action.payload 就像是請求的 body/query。

Reducer 飾演 Controller + Service + DAO

const reducer = function (prevState, action) {
    if (!prevState) {
        return {}
    }
    switch(action.type) {
        // 分發處理
    }
    return prevState;
}
redux.createStore(reducer);
複製程式碼

相應請求會進入對應的控制器(就像 reducer 的 switch 判斷),而控制器內也會對請求攜帶的資訊(payload)做分析,來實現對資料庫的增刪改查。

設計原則

單一資料來源

整個應用的 state 被儲存在一棵 object tree 中,並且這個 object tree 只存在於唯一一個 store 中。

一般情況下 createStore 建立的 store ,將作用於整個應用

State 是隻讀的

唯一改變 state 的方法就是觸發 action,action 是一個用於描述已發生事件的普通物件。

避免人為的操作 state 變數,造成狀態變化不可預測的情況,人為修改 state 的方式被切斷了。

至於如何實現,點我看原始碼

createStore 的操作,會建立一個內部變數 state, 由於 return 出來的 dispatchsubscribe 方法保持了對 state 變數的引用,所以 state 會以閉包的形式存活下來。

使用純函式來執行修改

為了描述 action 如何改變 state tree ,你需要編寫 reducers。

使用純函式,可測試性和可維護性得到了保障。

完成與檢視層的繫結

Redux 職責是狀態管理,並非只限定於前端開發,要使用到 web 開發當中,還缺少一個部分,完成 MVC 中剩下的一些操作(渲染頁面)。

web架構
v-redux架構

兩個重要的 API

  • store.subscribe(f) - 釋出訂閱模型, 方法 f 會在 dispatch 觸發後執行
  • store.getState() - return 出當前完整的 state 樹

簡單粗暴的方式

const store = redux.createStore(reducer);
const ComponentA = Regular.extend({
    config() {
        const ctx = this;
        store.subscribe(function () {
            const state = store.getState();
            const mapData = {
                clicked: state.clicked,
            }
            Object.assign(ctx.data, mapData);
            ctx.$update();
        });
    }
});
複製程式碼

兩個問題

  1. Store 獲取
  2. mapData 與 $update 操作不可複用

更優雅的繫結方式

<StoreProvier store={store}>
    <Component></Component>
</StoreProvier>
複製程式碼
const Component = connect({
    mapState(state) {
        return {
            clicked: state.clicked,
        }
    }
})(Regular.extend({
    config() {
    }
}))
複製程式碼

1. StoreProvier

Regular.extend({
    name: 'StoreProvider',
    template: '{#include this.$body}',
    config({store} = this.data) {
       if (!store) {
           throw new Error('Provider expected data.store to be store instance created by redux.createStore()')
       }
       
       store.subscribe(() => {
           this.$update();
       });
    }
})
複製程式碼

2. connect

統一從最外層的 StoreProvider 元件獲取 store,保證單一資料來源

function getStore(ctx) {
    let parent = ctx.$parent;
    while(true) {
        if (!parent) {
            throw new Error('Expected root Component be Provider!')
        }

        if (parent.data.store) {
            return parent.data.store;
        }

        parent = parent.$parent;
    }
}

function connect({
    mapState = () => ({}),
    dispatch
} = {}) {
    return (Component) => Component.implement({
        events: {
            $config(data = this.data) {
                const store = getStore(this);
                const mapStateFn = () => {
                    const state = store.getState();
                    const mappedData = mapState.call(this, state);
                    mappedData && Object.assign(this.data, mappedData);
                }
                mapStateFn();

                const unSubscribe = store.subscribe(mapStateFn);
                
                if (dispatch) {
                    this.$dispatch = store.dispatch;
                }
                
                this.$on('destroy', unSubscribe);
            } 
        }
    });
}
複製程式碼

至此回過頭看 regualr-redux 的架構圖,發現正是 StoreProvierconnect 操作幫助 redux 完成了與 MVC 相差的更新檢視操作。

總結

藉助 redux 與特定框架的聯結器,我們會發現,對特定 MVVM 框架的要求會變得很低 -- mapState 操作可以完成 類似 Vue 中 computed/filter 的操作。

所以,如今還在半殘廢中的微信小程式也很適合基於這套思路來結合 redux (逃)。

帶來的好處是,你可以安心維護你的模型層,不用擔心 data 過大導致的髒值檢查緩慢,也不需要考慮一些邏輯相關的資料不放入 data 那該放入何處。

相信閱讀此文,你會對 Redux 解決問題的方式有了一定的認識,而繼續深入的一些方向有:
* middleway 實現原理,redux-logger 和 redux-thunk 等實現方案;
* State 正規化化
* Presentational and Container Components
* 單頁的 redux 架構設計

全文完 ;)

by 君羽

PS:戳我檢視原文


相關文章