美文网首页程序员今日看点前端
Reducer 最佳实践,Redux 开发最重要的部分

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

作者: 齐修_qixiuss | 来源:发表于2016-12-19 12:21 被阅读15517次

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

    相关文章

      网友评论

        本文标题:Reducer 最佳实践,Redux 开发最重要的部分

        本文链接:https://www.haomeiwen.com/subject/ffrnmttx.html