上一篇中的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目录中,目录结构变为:
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);
最终结果和前两篇是一样的,如下图数字会跟随点击的按钮发生变化。例子本身的结果不重要。重要的是代码的结构更加工程化。
上一篇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提示:
目录结构更新为:
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的结构变成:
读取state值时:
store.getState().changeNumber.number
store.getState().toggleAlert.showAlert
很简单,combineReducers就这点内容。补充一句,combineReducers并没规定只能连接到顶层Reducer里,你可以根据实际的业务逻辑封装出任意层级的Reducer。这样业务代码的封装性和可读性会变的更好。
网友评论