簡介
Redux 中的 combineReducers
能讓我們很方便地把多個 reducers 組合起來,成為一個新的 reducer。
然而,隨著我們的應用變得越來越複雜,combineReducers
有可能不能滿足我們的需求。
正如 Redux 官方文件所說:
This helper is just a convenience! You can write your own combineReducers that works differently, or even assemble the state object from the child reducers manually and write a root reducing function explicitly, like you would write any other function.
combineReducers
只是方便我們使用而已,我們可以自定義一個完全不同的 combineReducers
來滿足我們特殊的需求。
原理
我們先回憶一下 reducer 的寫法:
const reducer = (oldState, action) => newState;
reducer 是一個普通的函式,接受兩個引數:oldState
和 action
,然後返回一個 newState
。
為了把多個 reducers 組合起來,我們通常會用 Redux 自帶的 combineReducers
來實現:
const rootReducer = combineReducers({
key1: key1Reducer,
key2: key2Reducer
});
先留意一下我們傳了什麼給 combineReducers
:
{
key1: function(state.key1, action) { /*...*/ },
key2: function(state.key2, action) { /*...*/ },
}
好了,讓我們先來想一想,經過 combineReducers
的處理之後,我們得到了什麼呢?
不用想了,很顯然我們得到了一個新的 reducer。
那這個新的 reducer 又長什麼樣呢?
const rootReducer = (oldState, action) => newState;
你應該不會驚訝,因為所有 reducer 都長這個樣子,即使它是已經被組合過的 reducer,它也是長這個樣子。
現在你應該猜到 combineReducers
做了什麼了吧?其實它最基本形態是這樣子的:
function combineReducers(reducers) {
return function (state, action) { /*...*/ };
}
它接受 reducers
作為引數,然後返回一個標準的 reducer 函式。
注意:
其實到了這一步,我們就可以自定義combineReducers
了,我們完全可以寫一個類似的函式,然後在裡面寫各種switch...case
語句來達到自定義的目的。
但我覺得我們還是先看看 Redux 自帶的combineReducers
做了什麼比較好,因為我們自定義的combineReducers
很有可能需要原來的功能。
還記得我剛才叫你留意的地方嗎?沒錯,就是下面這個:
// reducers
{
key1: function(state.key1, action) { /*...*/ },
key2: function(state.key2, action) { /*...*/ }
}
我們來回想一下 store.dispatch(action)
的過程:當一個 action
觸發的時候,所有 reducers 都應該響應這個 action
,做出相應的改變,最後返回一個新的 store
。
對著上面這個結構,我們其實很容易就能寫出這樣的效果,還能加上一些其他的處理:
function reCombineReducers(reducers) {
return function (state, action) {
switch (action.type) {
case SP_ACTION:
return Object.assign({}, state, { /* do something */ });
default:
return Object.keys(reducers)
.map(k => ({ [k]: reducers[k](state[k], action) }))
.reduce((prev, next) => Object.assign({}, prev, next));
}
}
}
上面的例子模擬了原來 combineReducers
的功能,還對 SP_ACTION
進行了特殊的處理,很簡單吧!
然而,上面的例子雖然模擬了 combineReducers 的功能,但失去了 combineReducers 的檢查物件變化的功能,因為現在的 default block 中會返回一個全新的物件。
有沒有方法可以既保留 combineReducers 的全部功能,又能擴充套件它呢?
其實很簡單,我們只要利用 combineReducers 返回的函式就可以了!
(感謝 liximomo 指出上面例子中的缺陷)
function reCombineReducers(reducers) {
let fn = combineReducers(reducers);
return function (state, action) {
switch (action.type) {
case SP_ACTION:
return Object.assign({}, state, { /* do something */ });
default:
return fn(state, action);
}
}
}
例項
按照 Redux 的原則,不同的 reducer 應該相互獨立的,它們之間不應該有任何依賴。
這個原則看著是很美好的,但在實際使用中還是會有一些例外的情況。
一個很簡單的例子,也是我遇到過的例子,就是實現一個簡單的表格 (其實我的情況複雜的多,需要實現類似 Excel 那樣的操作,同時支援其他額外的功能)。
我們先來設計一下 state
:
// state
{
rows: { ... },
cells: { ... },
data: { ... }
}
rows
, cells
, data
都會響應一些特定的 action
(如 CHANGE_ROW_PROPS
, CHANGE_CELL_PROPS
, CHANGE_DATA
),做出相應的改變,這些都是我們所期望的。
然而,當出現一些特殊的 action (如 GET_TABLE_SUCCESS
,表示成功從服務端獲取資料) 的時候,災難就出現了:
所有的 reducer 都需要監聽 GET_TABLE_SUCCESS
這個 action,這意味著如果我們有 n 個 reducer 的話,我們就需要修改 n 個檔案!
當我再加上 UPDATE_TABLE_SUCCESS
,REMOVE_TABLE_SUCCESS
之類的 action
時,我要再修改 n 個檔案!
這不合理啊,為什麼我加一個簡單的功能,需要修改這麼多檔案,最重要的是,這些修改都是非常類似!
這時候,我們就需要自定義 combineReducers
來解決我們的需求拉:
function reCombineReducers(reducers) {
let fn = combineReducers(reducers);
return function (state, action) {
switch (action.type) {
case GET_TABLE_SUCCESS:
case UPDATE_TABLE_SUCCESS:
return Object.assign({}, state, action.payload.table);
case REMOVE_TABLE_SUCCESS:
return initState;
default:
return fn(state, action);
}
}
}
const table = reCombineReducers({
sections,
suites,
rows,
cells,
toys,
data,
logics
})
怎麼樣,是不是比修改多個檔案舒服很多?
(完)
出處
http://scarletsky.github.io/2...
參考資料
http://redux.js.org/docs/api/...
https://github.com/reactjs/re...