3.2.2 Redux实例
您可以在百度里搜索“深入浅出React和Redux 艾草文学(www.321553.xyz)”查找最新章节!
3.2.2 Redux实例
单纯只看书面介绍难以理解Redux如何工作的,让我们还是通过例子来介绍。
前面我们用Flux实现了一个ControlPanel的应用,接下来让我们用Redux来重新实现一遍同样的功能,通过对比就能看出二者的差异。
React和Redux事实上是两个独立的产品,一个应用可以使用React而不使用Redux,也可以使用Redux而不使用React,但是,如果两者结合使用,没有理由不使用一个名叫react-redux的库,这个库能够大大简化代码的书写。
不过,如果一开始就使用react-redux,可能对其设计思路完全一头雾水,所以,我们的实例先不采用react-redux库从最简单的Redux使用方法开始,初步改进,循序渐进地过渡到使用react-redux。
最基本的Redux实现,存在于本书对应Github的chapter-03/redux_basic目录中,在这里我们只关注使用Redux实现和使用Flux实现的不同的文件。
首先看关于action对象的定义,和Flux一样,Redux应用习惯上把action类型和action构造函数分成两个文件定义,其中定义action类型的src/ActionTypes.js和Flux版本没有任何差别,但是src/Actions.js文件就不大一样了,代码如下:
import * as ActionTypes from './ActionTypes.js';
export const increment = (counterCaption) => {
return {
type: ActionTypes.INCREMENT,
counterCaption: counterCaption
};
};
export const decrement = (counterCaption) => {
return {
type: ActionTypes.DECREMENT,
counterCaption: counterCaption
};
};
和Flux的src/Actions.js文件对比就会发现,Redux中每个action构造函数都返回一个action对象,而Flux版本中action构造函数并不返回什么,而是把构造的动作函数立刻通过调用Dispatcher的dispatch函数派发出去。
这是一个习惯上的差别,接下来我们会发现,在Redux中,很多函数都是这样不做什么产生副作用的动作,而是返回一个对象,把如何处理这个对象的工作交给调用者。
在Flux中我们要用到一个Dispatcher对象,但是在Redux中,就没有Dispatcher这个对象了,Dispatcher存在的作用就是把一个action对象分发给多个注册了的Store,既然Redux让全局只有一个Store,那么再创造一个Dispatcher也的确意义不大。所以,Redux中“分发”这个功能,从一个Dispatcher对象简化为Store对象上的一个函数dispatch,毕竟只有一个Store,要分发也是分发给这个Store,就调用Store上一个表示分发的函数,合情合理。
我们创造一个src/Store.js文件,这个文件输出全局唯一的那个Store,代码如下:
import {createStore} from 'redux';
import reducer from './Reducer.js';
const initValues = {
'First': 0,
'Second': 10,
'Third': 20
};
const store = createStore(reducer, initValues);
export default store;
在这里,我们接触到了Redux库提供的createStore函数,这个函数第一个参数代表更新状态的reducer,第二个参数是状态的初始值,第三个参数可选,代表Store Enhancer,在这个简单例子中用不上,在后面的章节中会详细介绍。
确定Store状态,是设计好Redux应用的关键。从Store状态的初始值看得出来,我们的状态是这样一个格式:状态上每个字段名代表Counter组件的名(caption),字段的值就是这个组件当前的计数值,根据这些状态字段,足够支撑三个Counter组件。
那么,为什么没有状态来支持Summary组件呢?因为Summary组件的状态,完全可以通过把Counter状态数值加在一起得到,没有必要制造冗余数据存储,这也符合Redux“唯一数据源”的基本原则。记住,Redux的Store状态设计的一个主要原则:避免冗余的数据。
接下来看src/Reducer.js中定义的reducer函数,代码如下:
import * as ActionTypes from './ActionTypes.js';
export default (state, action) => {
const {counterCaption} = action;
switch (action.type) {
case ActionTypes.INCREMENT:
return {...state, [counterCaption]: state[counterCaption] + 1};
case ActionTypes.DECREMENT:
return {...state, [counterCaption]: state[counterCaption] - 1};
default:
return state
}
}
和Flux应用中每个Store注册的回调函数一样,reducer函数中往往包含以action.type为判断条件的if-else或者switch语句。
和Flux不同的是,多了一个参数state。在Flux的回调函数中,没有这个参数,因为state是由Store管理的,而不是由Flux管理的。Redux中把存储state的工作抽取出来交给Redux框架本身,让reducer只用关心如何更新state,而不要管state怎么存。
代码中使用了三个句点组成的扩展操作符(spread operator),这表示把state中所有字段扩展开,而后面对counterCaption值对应的字段会赋上新值,像下面这样的代码这样:
return {...state, [counterCaption]: state[counterCaption] + 1};
上面的代码逻辑上等同于下面的代码:
const newState = Object.assign({}, state);
newState[counterCaption] ++;
return newState;
像上面这样写,创造了一个newState完全复制了state,通过对newState的修改避免了对state的修改,不过这样写显得冗长,使用扩展操作符看起来更清晰简洁。
提示
扩展操作符(spread operator)并不是ES6语法的一部分,甚至都不是ES Next语法的一部分,但是因为其语法简单,已经被广泛使用,因为babel的存在,也不会有兼容性问题,所以我们完全可以放心使用。
和Flux很不一样的是,在reducer中,绝对不能去修改参数中的state,如果我们直接修改state并返回state,代码如下,注意下面的代码不是正确写法:
export default (state, action) => {
const {counterCaption} = action;
switch (action.type) {
case ActionTypes.INCREMENT:
state[counterCaption] ++;
case ActionTypes.DECREMENT:
state[counterCaption] --;
}
return state;
}
像上面这样写,似乎更简单直接,但实际上犯了大错,因为reducer应该是一个纯函数,纯函数不应该产生任何副作用。
接下来,我们看View部分,View部分代码都在src/views目录下。看看src/views/ControlPanel.js,作为这个应用最顶层的组件ControlPanel,内容和Flux例子中没有任何区别。然后是Counter组件,存在于src/views/Counter.js中,这就和Flux不大一样了,首先是构造函数中初始化this.state的来源不同,代码如下:
import store from '../Store.js';
class Counter extends Component {
constructor(props) {
super(props);
. . .
this.state = this.getOwnState();
}
getOwnState() {
return {
value: store.getState()[this.props.caption]
};
}
和Flux例子一样,在这个视图文件中我们要引入Store,只不过这次是我们引入的Store不叫CounterStore,而是一个唯一的Redux Store,所以名字就叫store,通过store.getState()能够获得store上存储的所有状态,不过每个组件往往只需要使用返回状态的一部分数据。为了避免重复代码,我们把从store获得状态的逻辑放在getOwnState函数中,这样任何关联Store状态的地方都可以重用这个函数。
和Flux实现的例子一样,仅仅在构造组件时根据store来初始化this.state还不够,要保持store上状态和this.state的同步,代码如下:
onChange() {
this.setState(this.getOwnState());
}
componentDidMount() {
store.subscribe(this.onChange);
}
componentWillUnmount() {
store.unsubscribe(this.onChange);
}
在componentDidMount函数中,我们通过Store的subscribe监听其变化,只要Store状态发生变化,就会调用这个组件的onChange方法;在componentWillUnmount函数中,我们把这个监听注销掉,这个清理动作和componentDidMount中的动作对应。
其实,这个增加监听函数的语句也可以写在构造函数中,但是为了让mount和unmount的对应看起来更清晰,在所有的例子中我们都把加载监听的函数放在component-DidMount中。
除了从store同步状态,视图中可能会想要改变store中的状态,和Flux一样,改变store中状态唯一的方法就是派发action,代码如下:
onIncrement() {
store.dispatch(Actions.increment(this.props.caption));
}
onDecrement() {
store.dispatch(Actions.decrement(this.props.caption));
}
上面定义了onIncrement和onDecrement方法,在render函数中的JSX中需要使用这两种函数,代码如下:
render() {
const value = this.state.value;
const {caption} = this.props;
return (
+
-
{caption} count: {value}
);
}
在render函数中,对于点击“+”按钮和“-”按钮的onClick事件,被分别挂上了onIncrement函数和onDecrement函数,所做的事情就是派发对应的action对象出去。注意和Flux例子的区别,在Redux中,action构造函数只负责创建对象,要派发action就需要调用store.dispatch函数。
组件的render函数所显示的动态内容,要么来自于props,要么来自于自身状态。
然后再来看看src/views/Summary.js中的Summary组件,其中getOwnState函数的实现代码如下:
getOwnState() {
const state = store.getState();
let sum = 0;
for (const key in state) {
if (state.hasOwnProperty(key)) {
sum += state[key];
}
}
return { sum: sum };
}
Summary组件的套路和Counter组件差不多,唯一值得一提的就是getOwnState函数的实现。因为Store的状态中只记录了各个Counter组件的计数值,所以需要在getOwn-State状态中自己计算出所有计数值总和出来。
在网页中看一下最终效果,和Flux例子没有任何区别。 深入浅出React和Redux