Reducer 最佳实践,Redux 开发最重要的部分

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 功能函数?

不要被什么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的代码保持结构上的精简。

如何设计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数据共享。

如何合并多个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本质上是为了实现专门数据专门管理,让数据管理更清晰。那么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中。

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,012评论 4 359
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,589评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 106,819评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,652评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 51,954评论 3 285
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,381评论 1 210
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,687评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,404评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,082评论 1 238
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,355评论 2 241
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,880评论 1 255
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,249评论 2 250
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,864评论 3 232
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,007评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,760评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,394评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,281评论 2 259

推荐阅读更多精彩内容