reducer就是实现(state, action) => newState
的纯函数,也就是真正处理state的地方。值得注意的是,Redux并不希望你修改老的state,而且通过直接返回新state的方式去修改。
在讲如何设计reducer之前,先介绍几个术语:
✦ reducer:实现(state, action) -> newState
的纯函数,可以根据场景分为以下好几种
✦ root reducer:根reducer,作为createStore的第一个参数
✦ slice reducer:分片reducer,相对根reducer来说的。用来操作state的一部分数据。多个分片reducer可以合并成一个根reducer
✦ higher-order reducer:高阶reducer,接受reducer作为参数的函数/返回reducer作为返回值的函数。
✦ case function:功能函数,接受指定action后的更新逻辑,可以是简单的reducer函数,也可以接受其他参数。
reducer的最佳实践主要分为以下几个部分
✦ 抽离工具函数,以便复用。
✦ 抽离功能函数(case function),精简reducer声明部分的代码。
✦ 根据数据类别拆分,维护多个独立的slice reducer。
✦ 合并slice reducer。
✦ 通过crossReducer在多个slice reducer中共享数据。
✦ 减少reducer的模板代码。
接下来,我们详细的介绍每个部分
抽离工具函数,几乎在任何一个项目中都需要。要抽离的函数需要满足以下条件:
✦ 纯净,和业务逻辑不耦合
✦ 功能单一,一个函数只实现一个功能
由于reducer都是对state的增删改查,所以会有较多的重复的基础逻辑,针对reducer来抽离工具函数,简直恰到好处。
// 比如对象更新,浅拷贝
export const updateObject = (oldObj, newObj) => {
return assign({}, oldObj, newObj);
}
// 比如对象更新,深拷贝
export const deepUpdateObject = (oldObj, newObj) => {
return deepAssign({}, oldObj, newObj);
}
工具函数抽离出来,建议放到单独的文件中保存。
不要被什么case function吓到,直接给你看看代码你就清楚了,也是体力活,目的是为了让reducer的分支判断更清晰。
// 抽离前,所有代码都揉到slice reducer中,不够清晰
function appreducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
...
...
return newState;
case 'TOGGLE_TODO':
...
...
return newState;
default:
return state;
}
}
// 抽离后,将所有的state处理逻辑放到单独的函数中,reducer的逻辑格外清楚
function addTodo(state, action) {
...
...
return newState;
}
function toggleTodo(state, action) {
...
...
return newState;
}
function appreducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(state, action);
case 'TOGGLE_TODO':
return toggleTodo(state, action);
default:
return state;
}
}
case function就是指定action的处理函数,是最小粒度的reducer。
抽离case function,可以让slice reducer的代码保持结构上的精简。
上一篇 关于state的博客 已经提过,我们需要对state进行拆分处理,然后用对应的slice reducer去处理对应的数据,比如article相关的数据用articlesReducer去处理,paper相关的数据用papersReducer去处理。
这样可以保证数据之间解耦,并且让每个slice reducer保持代码清晰并且相对独立。
比如好奇心日报有articles、papers两个类别的数据,我们拆分state并扁平化改造
{
// 扁平化
entities: {
articles: {},
papers: {}
},
// 按类别拆分数据
articles: {
list: []
},
papers: {
list: []
}
}
为了对state.articles和state.papers分别进行管理,我们设计两个slice reducer,分别是articlesReducer和papersReducer
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[UPDATE_ARTICLES_LIST]: updateArticelsList(articles, action)
}
// ------------------------------------
// reducer
// ------------------------------------
// !!!值得注意的是,对于articlesReducer来说,它并不知道state的存在,它只知道state.articles!!!
// 所以articlesReducer完成的工作是(articles, action) => newArticles
export function articlesReducer(articles = { list: [] }, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(articles, action) : articles
}
// papersReducer类似,就不贴代码了。
由于我们的state进行了扁平化改造,所以我们需要在case function中进行normalizr化。
根据state的拆分,设计出对应的slice reducer,让他们对自己的数据分别管理,这样后代码更便于维护,但也引出了两个问题。
✦ 拆分多个slice reducer,但createStore只能接受一个reducer作为参数,所以我们怎么合并这些slice reducer呢?
✦ 每个slice reducer只负责管理自身的数据,对state并不知情。那么articlesReducer怎么去改变state.entities的数据呢?
这两个问题,分别引出了两部分内容,分别是:slice reducer合并、slice reducer数据共享。
redux提供了combineReducer方法,可以用来合并多个slice reducer,返回root reducer传递给createStore使用。直接上代码,非常简单。
combineReducers({
entities: entitiesreducer,
// 对于articlesReducer来说,他接受(state, action) => newState,
// 其中的state,是articles,也就是state.articles
// 它并不能获取到state的数据,更不能获取到state.papers的数据
articles: articlesReducer,
papers: papersReducer
})
传递给combineReducer的是key-value 键值对
,其中键表示传递到对应reducer的数据,也就是说:slice reducer中的state并不是全局state,而是state.articles/state.papers
等数据。
slice reducer本质上是为了实现专门数据专门管理,让数据管理更清晰。那么slice reducer间如何共享数据呢?
举个例子,我们异步获取article的时候,会附带将comments也带过来,那么我们在articlesReducer中怎么去维护这份comments数据?
// 不好的方法
// 我们通过两次dispatch来分别更新comments和article
// 缺点是:slice reducer之间严重耦合,代码不容易维护
dispatch(updateComments(comments));
dispatch(updateArticle(article)));
那么有什么更好的办法呢?我们能不能在articlesReducer处理之后,将action透传给commentsReducers呢?看看如下代码
// 定义一个crossReducer
function crossReducer(state, action) {
switch (action.type) {
// 处理指定的action
case UPDATE_COMMENTS:
return Object.assign({}, state, {
// 这儿是关键,相当于透传到commentsReducer,然后让commentsReducer去处理对应的逻辑。
// 这样的话
// crossReducer不关心commentsReducer的逻辑
// articlesReducer也不用去关心commentsReducer的逻辑
comments: commentsReducer(state.comments, action)
});
default:
return state;
}
}
let combinedReducer = combineReducers({
entities: entitiesreducer,
articles: articlesReducer,
papers: papersReducer
});
// 在其他reducer处理完成后,在进行crossReducer的操作
function rootReducer(state, action) {
let tempstate = combinedReducer(state, action),
finalstate = crossReducer(tempstate, action);
return finalstate;
}
当然,我们可以使用reduce-reducers这个插件来简化上面的rootReducer。
import reduceReducers from 'reduce-reducers';
export const rootReducer = reduceReducers(
combineReducers({
entities: entitiesreducer,
articles: articlesReducer,
comments: commentsReducer
}),
crossReducer
);
原理很简单,先执行某些slice reducer,执行完成后,再去执行crossReducer,而crossReducer本身不做任何的工作,只负责调用关联reducer,并且把数据传到关联reducer中。
每次写action/action creator/reducer
,都会写很多相似度很高的代码,我们是否可以通过一定封装,来减少这些样板代码呢?
比如我们定义一个createReducer的函数,用来创建slice reducer。如下所示:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
const todosreducer = createReducer([], {
'ADD_TODO': addTodo,
'TOGGLE_TODO': toggleTodo,
'EDIT_TODO': editTodo
});
也可以使用现成的比较好的方案,比如:redux-actions。给个简单的示例,更多的可以查看官方文档。
// 定义action及action creator
const {
increment,
descrement
} = createActions({
INCREMENT: (val) => val,
DECREMENT: (val) => val
});
// 定义reducer
const reducer = handleActions({
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
}, { counter: 0 });
减少样板代码之后,代码一下就变得清晰多了。
reducer的设计相对于state和action来说要复杂很多,他涉及拆分、合并、数据共享的问题。
本文介绍了怎样最佳实践的去设计reducer,按照上面的步骤下来,可以让你的reducer保持结构简单。
✦ 抽离工具函数,这个不用多说。
✦ 抽离case function,让slice reducer看起来更简洁。其中case function是最小粒度的reducer,是action的处理函数。
✦ 拆分slice reducer,这个是和state拆分匹配的,拆分slice reducer是为了实现专门数据专门管理,并且让slice reducer更加便于维护。
✦ 合并slice reducer,createStore只能接受一个reducer作为参数,所以我们用combineReducer将拆分后的slice reducer合并起来。先拆分再合并其实更多是为了工程上的便利。
✦ 使用crossReducer类似的功能,可以实现slice reducer间数据共享。
✦ 减少reducer的样板代码,这个不多说,使用redux-actions就挺好,但不建议新人这样做。
实际开发中,我个人更喜欢将action和reducer写在一个文件中,并且将redux相关的代码全部放到统一的目录中。
结合上一篇博客讲的 state设计,Redux基本的架构雏形就出来了,当然可以继续深入,比如结合按需加载、路由、数据持久化等等。