建议在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循环来对每一个reducer都进行了执行)。
例如下面的例子,我们想要跟踪应用中多个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
参考资料: