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基本的架构雏形就出来了,当然可以继续深入,比如结合按需加载、路由、数据持久化等等。