React高階元件原理與在Redux中的實踐
建議在github閱讀,我會及時更新部分內容。也歡迎star,issue
1.高階reducer的定義
高階reducer指的是一個函式,該函式接收一個reducer函式作為引數或者返回一個reducer函式作為函式的返回值。高階reducer也可以被看做為一個reducer工廠,combineReducers是高階reducer一個典型的例子。我們可以使用高階reducer函式來建立一個符合自己要求的reducer的函式。
2.為什麼要高階reducer函式
當應用功能變大的時候,在reducer函式中那些通用的邏輯就會出現重複。你會發現很多reducer都是處理同樣的邏輯,只是處理的資料不同而已,所以我們就會想著如何重複使用reducer函式中那些通用的邏輯,從而減小應用的程式碼。同時,有時候你想要在store中處理特定型別資料的多個”例項”(如下面的counter函式需要用於多個地方)。然而,redux的全域性store採用了一些權衡:雖然,我們可以很容易跟蹤整個應用的state狀態(for迴圈,所有的reducer都執行了一遍),但是,很難針對特定的action來更新特定的state的某一部分資料,特別是當你使用combineReducers(因為dispatch的時候,我們採用的是for迴圈來對
例如下面的例子,我們想要跟蹤應用中多個counter例項,分別為counterA,counterB,counterC。我們首先定義了counter這個reducer,同時使用了combineReducers來管理狀態:
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1 ;
default:
return state;
}
}
const rootReducer = combineReducers({
counterA : counter,
counterB : counter,
counterC : counter
});
針對這種情況存在一個問題,因為我們的combineReducer採用的是for迴圈來對所有的reducer使用相同的action都執行一遍(不知道的請點選這裡,檢視combineReducer部分分析)。所以如果我們dispacth({type:”INCREMENT”}),那麼,我們上面counterA,counterB,counterC都會執行一遍,而不是執行某一個reducer函式。所以我們需要使用一種機制來保證只有一個reducer會執行,而這種機制就是我們要說的高階reducer。
3.高階reducer的用法
指定一個reducer的最常用的方式就是使用一個字尾或者字首來產生一個reducer的action,或者將額外的資訊新增到action物件上。下面是幾個例子:
3.1 常規高階reudcer函式
//這是一個高階reducer,因為它會返回一個reducer函式作為返回值
function createCounterWithNamedType(counterName = '') {
return function counter(state = 0, action) {
switch (action.type) {
case `INCREMENT_${counterName}`:
return state + 1;
case `DECREMENT_${counterName}`:
return state - 1;
default:
return state;
}
}
}
//這裡和上面的高階reducer是一樣的,只是方式不同而已
function createCounterWithNameData(counterName = '') {
return function counter(state = 0, action) {
const {name} = action;
if(name !== counterName) return state;
//如果是我們關注的counterName名稱,那麼我們才會通過action的type進行處理
//否則原樣返回state
switch (action.type) {
case `INCREMENT`:
return state + 1;
case `DECREMENT`:
return state - 1;
default:
return state;
}
}
}
下面我們可以使用上面任意一個函式來產生我們自己的reducer,然後dispatch一個action,而該action只會影響我們關心的那部分的state的值:
const rootReducer = combineReducers({
counterA : createCounterWithNamedType('A'),
counterB : createCounterWithNamedType('B'),
counterC : createCounterWithNamedType('C'),
});
//redux的dispatch方法會遍歷所有的reducer來計算下一個狀態
//subscribe用於計算完成後進行回撥
store.dispatch({type : 'INCREMENT_B'});
//當你呼叫store的dispatch的時候我們會遍歷上面指定的所有的reducers,然後發現只有
//第二個reducer,即createCounterWithNamedType('B')能夠處理,其他reducer原樣返回
//state的當前狀態。注意:每一個應用只有一個store,combineReducers每一個key負責管理
//store的一部分狀態
console.log(store.getState());
// {counterA : 0, counterB : 1, counterC : 0}
// store中只有counterB發生變化
3.2 通用高階reudcer函式
我們也可以通過下面的方式來產生一個更加通用的高階reducer,該reducer同時接收一個指定的reducer函式以及一個name或者identifier:
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
//通用reducer工廠函式,接收一個reducer函式和一個name作為引數,返回一個通用reducer
function createNamedWrapperReducer(reducerFunction, reducerName) {
return (state, action) => {
const {name} = action;
//dispatch的這個action必須有一個name屬性,用於判斷你要執行哪一個reducer
const isInitializationCall = state === undefined;
if(name !== reducerName && !isInitializationCall) return state;
//如果傳遞的action.name不是該reducerName指定的reducer處理,那麼返回當前state
//否則通過我們的reducer函式來處理。和上面這個例子一樣
return reducerFunction(state, action);
}
}
const rootReducer = combineReducers({
counterA : createNamedWrapperReducer(counter, 'A'),
counterB : createNamedWrapperReducer(counter, 'B'),
counterC : createNamedWrapperReducer(counter, 'C'),
});
其實上面的這種邏輯已經有一個庫實現了,即
multireducer。
3.3 含有通用過濾函式的高階reudcer函式
//和上面不一樣的是,這個reducer工廠函式接收的第二個引數是一個函式,而不是一個reducerName,這個函式用於對我們dispatch這個action進行過來
function createFilteredReducer(reducerFunction, reducerPredicate) {
return (state, action) => {
const isInitializationCall = state === undefined;
const shouldRunWrappedReducer = reducerPredicate(action) || isInitializationCall;
//傳入我們的action到reducer的filter函式中,如果返回true,那麼我們會執行reducer函式,如果返回false,我們返回當前state狀態即可
return shouldRunWrappedReducer ? reducerFunction(state, action) : state;
}
}
const rootReducer = combineReducers({
// check for suffixed strings
counterA : createFilteredReducer(counter, action => action.type.endsWith('_A')),
// check for extra data in the action
counterB : createFilteredReducer(counter, action => action.name === 'B'),
// respond to all 'INCREMENT' actions, but never 'DECREMENT'
counterC : createFilteredReducer(counter, action => action.type === 'INCREMENT')
});
上面的combineReducers整合的這個rootReducer可以處理的action如下:
store.dispatch({type:"XXX_A"})
//如果你dispatch的這個action.type字尾為_A,那麼會通過counter進行處理(注意:這個counter肯定和我們上面的counter有點差異,它會多出很多switch的case,因為這裡對我們的action的type有依賴)
//但是這裡是type的判斷情況,如果是下面的name就不會對switch的case產生影響,因為對type沒有影響
store.dispatch({name:"B"})
//此時我們還可以新增type屬性來讓我們上面的counter產生作用,是DECREMENT/INCREMENT
store.dispatch({type:"INCREMENT"})
4.高階reducer的幾個通用庫
multireducer我上面已經說過了,你可以直接去官網檢視。很顯然,它實現了一種機制,解決我們了第二部分表述的那種dispatch一個action後,所有的reducer都執行了一遍的情況。具體使用檢視API。但是這個庫不是我想說的,我想說的是violet-paginator,我是因為看了這個庫的用法才深入瞭解了高階reducer的內容。
import { createPaginator } from 'violet-paginator';
import { createStore, combineReducers, applyMiddleware, compose } from 'redux';
const reducer = combineReducers({
recipes: createPaginator(config)
//很顯然createPaginator返回值必須是一個reducer函式,這個函式用於對分頁處理
})
下面是具體的config內容:
export default {
listId: 'recipes',
fetch: mockFetch,
//會使用這個fetch方法傳送請求,毋須使用者手動干預
pageParams: {
totalCountProp: 'totalCount'
//伺服器通過那個欄位來返回我們總的欄位個數
//https://sslotsky.gitbooks.io/violet-paginator/content/v/v2.0.0/single_list_configuration.html
},
initialSettings: {
pageSize: 1
}
}
config中的內容就是為我們的reducer工廠函式(高階函式)傳入了配置資訊,所有的操作都會在該工廠函式中完成,其中包括從伺服器端獲取資料(通過fetch函式來完成獲取資料)
其中元件中的用法是:
<VioletPaginator listId="recipes" />
<VioletDataTable listId="recipes" />
其中我比較糾結的不是如何用的,而是上面配置的listId=”recipes”的作用是什麼,所以我好奇的看了下內部的實現:
//返回一個字串
function actionType(t, id) {
return `${t}_${id}`
}
export const INITIALIZE_PAGINATOR = '@@violet-paginator/INITIALIZE_PAGINATOR'
//這裡只給出了一個actionTypes的值
export default function createPaginator(config) {
const { initialSettings } = registerPaginator(config)
const resolve = t => actionType(t, config.listId)
////呼叫resolve得到一個字串
return resolveEach(defaultPaginator.merge(initialSettings), {
[actionTypes.EXPIRE_ALL]: expire,
[resolve(actionTypes.INITIALIZE_PAGINATOR)]: initialize,
//resolve呼叫得到"@@violet-paginator/INITIALIZE_PAGINATOR_recipes"
[resolve(actionTypes.EXPIRE_PAGINATOR)]: expire
})
}
所以,從這裡你大概可以看出來,我們點選了VioletPaginator的時候,listId=”recipes”決定過了我們發出的這個action的資訊,而且應該是type資訊。我們在dispatch的時候,會將所有的combineReducers中的key對應的函式都執行一遍!
5.高階reducer的好處
通過上面的例子你也可以看到,對於counterA,counterB,counterC我們只用提供一個reducer函式即counter,但是我們卻可以複用這部分的邏輯。只要我們dispatch的這個action明確指定我們需要改變哪一部分state狀態即可,如:
store.dispatch({name:"A",type:"INCREMENT"})
//此時只會改變我們counterA對應的那部分的state的狀態,同時將狀態的值加1
參考資料: