Redux介绍之Reducer

13 May

上一篇中的Action只是个数据的载体,用于告知Reducer发生了什么事情,真正搞事情的还得靠Reducer,在Reducer里更新Store里的state。本篇就介绍一下Reducer,源码已上传Github,第三篇源代码请参照src/originReduxReducer和src/originReduxCombineReducer文件夹。

Reducer应该是个纯函数,即只要传入相同的参数,每次都应返回相同的结果。不要把和处理数据无关的代码放在Reducer里,让Reducer保持纯净,只是单纯地执行计算。

Reducer接收两个参数:旧的state和Action,返回一个新的state。即(state, action) => newState。有两个注意点:一是首次执行Redux时,你需要给state一个初始值。二是根据官网的说明,Reducer每次更新状态时需要一个新的state,因此不要直接修改旧的state参数,而是应该先将旧state参数复制一份,在副本上修改值,返回这个副本。

第一点的板式语法是在Reducer的函数声明里,用es6的默认参数给state赋初值。第二点的板式语法是用es6的结构赋值将旧state复制一份:

return {
     ...state,
    // 更新state中的值
};

在第二篇的源代码基础上继续修改源代码,将Reducer独立到reducers目录中,目录结构变为:

redux

reducers/number.js:

import * as constant from '../configs/action';

const initialState = {
    number: 0,
};

export default (state = initialState, action) => {
    switch (action.type) {
        case constant.INCREMENT:
            return {
                ...state,
                number: state.number + 1,
            };
        case constant.DECREMENT:
            return {
                ...state,
                number: state.number - 1,
            };
        case constant.CLEAR_NUM:
            return {
                ...state,
                number: 0,
            };
        default:
            return state;
    }
};

entries/originReduxReducer.js:

// 改前
const reducer = (state, action) => {
    if (typeof state === 'undefined') {
        return 0;
    }

    switch (action.type) {
        case constant.INCREMENT:
            return state + 1;
        case constant.DECREMENT:
            return state - 1;
        case constant.CLEAR_NUM:
            return 0;
        default:
            return state;
    }
};

const store = createStore(reducer);

// 改后
import reducer from '../reducers/number';

const store = createStore(reducer);

最终结果和前两篇是一样的,如下图数字会跟随点击的按钮发生变化。例子本身的结果不重要。重要的是代码的结构更加工程化。

Redux

上一篇Action有个Action Creator,Reducer也有个Reducer Creator概念,用switch-case比较Action.type代码太low了。这里的low不是指用es6的语法就高大上了,而是指代码会不够清晰,代码是写给人看的,顺便让机器跑一下。我们用Reducer Creator改写一下上面的Reducer代码。

抽出个lib/common.js,里面定义个createReducer共同函数:

export const createReducer = (initialState, handlers) => {
    return (state = initialState, action) => {
        if (handlers.hasOwnProperty(action.type)) {
            return handlers[action.type](state, action);
        } else {
            return state;
        }
    }
};

如果你对函数式编程不熟悉,我啰嗦几句解释一下。createReducer本质上是返回一个函数对象。返回的匿名函数签名和Reducer函数签名是一样的,等于是封装了Reducer。在匿名函数内匹配Action.type,并返回一个和Reducer函数签名一样的另一个匿名函数。如果匹配不到Action.type,就返回state,这意味着你在写业务代码的Reducer时,甚至都不用考虑switch-case里default的问题。不知道我解释清楚了没有,不明白的话,多看几遍就能睡着了…

现在Reducer里可以不用switch-case,用createReducer专注于业务逻辑了:

import * as constant from '../configs/action';
import { createReducer } from '../lib/common';

const initialState = {
    number: 0,
};

export default createReducer(initialState, {
    [constant.INCREMENT]: (state, action) => {
        return {
            ...state,
            number: state.number + 1,
        };
    },
    [constant.DECREMENT]: (state, action) => {
        return {
            ...state,
            number: state.number - 1,
        };
    },
    [constant.CLEAR_NUM]: (state, action) => {
        return {
            ...state,
            number: 0,
        };
    },
});

Reducer既然是用于根据业务逻辑更新state,那如何切分业务是个问题。Redux基于此,提供了combineReducers方法,可以将多个Reducer合并成一个。参数是多个Reducer的key-value对象:

combineReducers({
    reducer1: myReducer1,
    reducer2: myReducer2,
});

但通常会选择用Reducer名直接作为key,因此可以写成:

combineReducers({
    myReducer1,
    myReducer2,
});

例如,我们为页面增加一个和数字不同的业务,点击按钮显示alert提示:

redux

目录结构更新为:

redux

alert的Action部分请自行参照源代码,不赘述。lib/common.js里封装着上面介绍过的createReducer方法,不赘述。reducers目录里number.js的代码不变,alert.js的代码结构同number.js,不赘述。

reducers/index.js:

import { combineReducers } from 'redux';
import changeNumber from './number';
import toggleAlert from './alert';

export default combineReducers({
    changeNumber,
    toggleAlert,
});

现在Store里的state的结构变成:

redux

读取state值时:

store.getState().changeNumber.number
store.getState().toggleAlert.showAlert

很简单,combineReducers就这点内容。补充一句,combineReducers并没规定只能连接到顶层Reducer里,你可以根据实际的业务逻辑封装出任意层级的Reducer。这样业务代码的封装性和可读性会变的更好。

Leave a Reply

Your email address will not be published. Required fields are marked *