0%

Redux初体验

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。Redux可以作为任何UI层的store,所以它可以运行在Angular、Angular 2、 Vue、React甚至React-Native中。Redux 提供的订阅机制,可以与任何代码集成。这里仅仅记录一些Redux在React-Native中的实现。

为什么要用状态管理

在React中已经提供了state以及setState来实现页面的改动,那么为什么需要状态管理呢?无论是Mobx还是Redux都作为setState的替代品。那么state以及setState有什么缺陷,必须通过这种第三方的状态管理的介入去构建应用。

setState方法在React中是一个很重要的方法,每当调用setState的时候React会重新渲染render方法,其中调用state的值就会相应发生改变,也就完成了一次页面响应。在React的教程中,也大多是这种介绍。那么问题在哪里呢?

首先,setState并不能保证是一个同步的方法,有可能是同步的,也有可能是异步的。它是一个合成法方法,后续会调用一系列的方法来完成组件重新渲染,关于setState的问题详情可以看这里。总之,state的状态并不是可以预测的。其次,在实际的生产中,复杂的页面会导致render下的DOM结构非常复杂,会包含一百到几百行的JSX代码。假如这个时候去改变其中某一个button的选中状态,本意是只改动button这个组件,其他的组件保持当前状态不动。当调用setState的时候,它并不会智能的仅仅改变button的状态,而是重新渲染整个庞大的DOM树。虽然React在Virtual DOM做了优化,但这种大规模的无用渲染仍然会影响应用性能。当然有解决的方法,就是将大组件拆分,分成很多的小组件,然后在shouldComponentUpdate中去做优化。关于shouldComponentUpdate相关优化问题可以参考这里。还有的话就是,state数据与JSX混杂在同一个页面,耦合高十分不方便维护。一般来说页面的UI、逻辑、网络请求分开来写,既方便维护,代码逻辑也很清楚。

上述的问题只是本人在React-Native上实践遇到的,如果有任何错误,请及时通知我。

Redux思想

既然在state以及setState实践上遇到了问题,那么Redux是如何解决这些问题的呢?先来看看Redux的核心概念:Action、Reducer和Store。Store中存储了数据state,state这个对象就像 “Model”,区别是它并没有 setter(修改器方法),因此其它的代码不能随意修改它,造成难以复现的 bug。Action 就是一个普通 JavaScript 对象它是用来描述发生了什么。为了把 action 和 state 串起来,开发一些函数,reducer 只是一个接收 state 和 action,并返回新的 state 的函数。 以上差不多就是Redux的全部思想了,Store、Action、Reducer各司其职配合工作。

在这里还要强调Redux中的三个原则:单一数据源、State 是只读的、使用纯函数来执行修改。

  1. 单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
  2. State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
  3. 使用纯函数来执行修改:为了描述 action 如何改变 state tree ,你需要编写 reducers。

在React-Native中使用

既然已经了解了Redux的基础概念,那么就在React-Native中使用它,才能进一步理解Redux是如何运作的。使用Redux实现一个简单的计数器。

就是这样的一个简单的例子,点击➕号数字加一,点击➖数字减一。当然在这里是可以用setState来实现的,为了学习Redux还是将它按照Redux的规范来编写。

首先要先引入redux,在项目的根目录执行

1
npm install --save redux

构建UI

首先我们先构建出这个UI页面,只要是接触过React-Native。那么是很容易实现的,大致的代码是这个样子的:

1
2
3
4
5
6
7
8
9
10
11
12
export default class Count extends Component<Props> {
render() {
const { number, addNumber, inNumber } = this.props;
return (
<View style={styles.container}>
<Text style={styles.welcome}>0</Text>
<Text style={styles.instructions} onPress={() => 加一}>+</Text>
<Text style={styles.instructions} onPress={() => 减一}>-</Text>
</View>
);
}
}

UI出现了,那么就差点击事件的处理了。

Action

这里我们需要发起一个事件,在这里点击的时候要让数字加一,所以首先要构建一个Action描述一个这个事情。

1
2
3
4
5
// action
export const addNumber = number => ({
type: 'INCREMENT',
number
})

type相当于key,作为Action的一个标识,注意这里的type是不可以有重复的,保证其唯一性。还接受了一个参数number,就是点击的时候,要在当前的number基础上加一。

Reducer

上面的Action仅仅表示了要发生的事情,并没有描述这个事情该怎么做。Reducer的功能就是去具体的实现state的变化,在这里就是承担了加一这个事情。记住,Reducer是一个纯函数,它接收state和action作为参数,并且返回一个新的state。也即是(state, action) => newState。

1
2
3
4
5
6
7
8
9
10
11
// reducer
export default function changeNumber(state=0, action) {
switch (action.type) {
case 'INCREMENT':
return Object.assign({}, state, {
number: action.number + 1
});
default:
return state;
}
}

还记得之前的Action设置的type吗?在Reducer中它作为区分不同的Action的一个标识,Reduce通过不同的type可以处理很多的Action。注意到Reducer返回了一个新的对象,虽然只改变了state中的一个字段。这里是通过Object.assign方法来实现的,也可以使用ES6中的{..state, number: action.number + 1}来完成这一操作。

现在完成了➕事件的处理,就差把onPress事件和Action连接起来了。

Store

在一个应用中只存在一个store,为了方便所有子组件访问该store,一般来说会在顶层组件中提供Provider组件,以便于子组件可以通过props的方式访问到store,可以自己实现一个Provider,一般的情况使用的是第三方组件react-redux。在项目的根目录中引用该组件

1
npm install --save react-redux

在项目的入口文件中,用Provider将UI组件包裹起来

1
2
3
<Provider>
<Count />
</Provider>

这里还有没将store传入Provider中,首先要创建一个store,通过Redux提供的createStore方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { createStore } from 'redux';
import { Provider } from 'react-redux';
import rootReducer from './src/reducers/index';

export default class App extends Component<Props> {
render() {
const store = createStore(rootReducer);
return (
<Provider store={store}>
<Count />
</Provider>
);
}
}

createStore方法接受一个Reducer,将刚才写的Reducer传入其中即可完成一个store创建。下面就是将store与UI关联起来。在最开始写完的UI页面中加入如下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { connect } from 'react-redux';
import { addNumber } from './actions/countAction'

type Props = {};
class Count extends Component<Props> {
render() {
const { number, plus } = this.props;
return (
<View style={styles.container}>
<Text style={styles.welcome}>{number}</Text>
<Text style={styles.instructions} onPress={() => plus(number)}>+</Text>
<Text style={styles.instructions} onPress={() => 减一}>-</Text>
</View>
);
}
}

function mapStateToProps(state){
return {
number: state.number,
};
}

function mapDispatchToProps(dispatch){
return {
plus: id => {
dispatch(addNumber(id))
}
}
}

export default connect(mapStateToProps,mapDispatchToProps)(Count);

主要是通过connect这个方法将store中的state以及action都传递给UI页面,可以通过props的方式访问store。在mapStateToProps以及mapDispatchToProps中定义参数名称以方便使用。完成以上所有代码之后,即可尝试运行代码,点击➕是否可以正常显示,减法和加法是同样的流程,这里并不做说明。

到这里就完成了一个由Redux构建的应用,可以看到为了实现一个方法的调用,做了很多的工作,从Action到Reducer再到state然后在渲染页面。可以说Redux的实现十分的繁琐,所以在具体的生产环节中,一定要思考清楚到底需不需要Redux,这里也有何时使用Redux的说明以供参考。

Middleware

关于Redux的中间件了解的并不多,这里只做简单的介绍。中间件实现的即是在Action与Reducer中间做一些操作的功能。比如,需要一个异步请求、需要调试每一次Actions执行的时候打印日志。这些都可以使用中间件完成。比如常用的redux-thunk、redux-logger等等。如果还是不知道中间件是做什么的,那么在项目中引用redux-logger

1
npm install --save redux-logger

完成之后,在创建store的时候将中间件添加进去

1
2
import logger from 'redux-logger';
const store = createStore(rootReducer, applyMiddleware(logger));

之后打开React-Native的调试页面,点击➕即可发现,控制台会自动的打印出相关的state信息,并不需要在代码中添加console.log操作,确实是极大的方便了调试。

redux-thunk主要是用来实现异步操作,比如网络请求之类的场景这里就不一一做尝试了。

总结

关于Redux我了解也不是很多,只能做一些简单的demo。在上述例子中并没有介绍Redux其他的应用技巧,在后续的学习中,逐步了解这些技巧的原理后再做记录。Redux确实在开发流程上做出了很多的规范,无论它是否真的适合生产中应用,都是只得学习它的思想的。

参考资料