一、redux解决问题
react在使用的过程中,主要是利用组件内的state来存储状态,在多个组件共享数据的时候,每个组件内都得保存相同一份数据,会造成数据重复冗余;而利用props进行组件间的通讯,在组件比较简单的时候,该方法倒没什么不妥,但随着我们的应用越来越大,越来越复杂,单纯的靠props进行组件间的通讯,会增加代码的复杂度和可读性,但查询数据源bug的时候,会变得极其复杂;更严格的数据流控制,是解决这个问题的所在。
二、Redux的基本原则
- 唯一数据源
- 保持状态只读
- 数据改变只能通过纯函数完成
让我们逐一解释一下三个原则。
1. 唯一数据源
唯一数据源是指应用的状态数据只存在唯一的Store上,如果数据存在多个store上,容易造成数据冗余,而且也容易导致数据一致性方面出现问题,而且,多个Store上面的数据如果存在依赖性,会增加应用的复杂程度,容易带来新的问题;当然,Redux并没有阻止一个应用拥有多个store,但这样不仅没有任何好处,甚至还不如一个store更容易组织代码。
这个唯一Store上的状态就是一个树形的形象,每个组件上往往只是树形对象上的一部分,而如何设计Store上的树形对象,就是Redux中的核心问题。
2.保持状态只读
要驱动用户界面渲染,就要改变应用的状态,保持状态只读,就是说不能直接去修改状态,要修改Store上的状态,需要通过派发一个action去处理;action会创建一个新的状态对象返回给Redux,有Redux完成新的状态的组装。
3.数据改变只能通过纯函数完成
所谓纯函数,就是不对外界产生任何副作用的函数;这里所说的纯函数,就是Reducer;Reducer并不是Redux的特有术语,是计算机的一个通用概念,就好比是JavaScript中的reduce(fn,init)函数,里面接收的回调函数fn就是一个Reducer;
在Redux中,每个reducer的函数签名如下所示:
reducer(state, action)
第一个参数state是当前的状态,第二个参数action是收到的action对象,而reducer函数所要做的事情,就是根据state和action的值产生一个新的对象返回,注意reducer必须是一个纯函数,也就是说,函数的返回结果只能由state和action决定,而且不产生任何副作用,也不能修改参数state和action对象。
例如:
function reducer(state, action) => {
const {targetKey} = action; //获取目标key的值
switch (action.type) {
case ActionTypes.typeOne:
return {...state, [targetKey]: state[targetKey] + 1};
case ActionTypes.typeTwo:
return {...state, [targetKey]: state[targetKey] - 1};
default:
return state
}
}
从上面的例子,可以看出,reducer函数不仅接收action,还接收state为参数,这就是说,Redux只负责计算状态,不负责保存状态。
三、Redux实例
为了方便,直接用create-react-app工具来初始化项目,执行下面指令前,必须保证我们的电脑已经安装了node.js;
npm install --global create-react-app
在命令行窗口中执行以上语句,安装create-react-app工具,安装成功后,可以得到如截图的内容;
接下来我们在命令行执行下面的指令,创建测试使用的应用;
create-react-app react-redux-app
创建成功后,进入项目目录,启动应用;
cd react-redux-app
npm start
这个命令启动一个开发者模式的服务器,同时也会让你的浏览器自动打开一个网页,指向本机http://localhost:3000/
create-react-app指令安装成功截图:
启动应用成功截图:
应用启动后的初始界面:
因为个人习惯,一般我都会执行 npm run eject,该指令的作用是,就是把潜藏在react-scripts 中的一系列技术找配置都“弹射”到应用的顶层,然后就可以研究这些配置细节了,而且可以更灵活地定制应用的配置。在react和redux结合使用的时候,没有理由不选择使用react-redux库,这样能大大节省代码的书写,不过从一开始我们不直接使用它,不然会对其内部设计一头雾水,所以先从最简单的redux开始使用,一步步改进,循循渐进地过度到react-redux。下面是项目目录结构:
其中,Store.js相当于MVC架构里面的M,views文件夹相当于V,Reducer.js相当于C,至于Actions和ActionTypes,可以理解用用户的行为;接下来需要执行npm install redux安装redux,我们从入口文件讲解实例的内容;
首先是index.js文件,文件先引入react和react-dom,再将ControlPanel组件挂载渲染到目标div;
import React from 'react';
import ReactDOM from 'react-dom';
import ControlPanel from './views/ControlPanel'
import './index.css';
ReactDOM.render(<ControlPanel />, document.getElementById('root'));
在Store.js文件中,通过引入redux的createStore函数,以及Reducer处理函数,创建并返回一个store,createStore(reducer, initValues)中的reducer是处理派发出来的action函数,initialValues为初始值,也就是组件所共享的数据结构;
import {createStore} from 'redux'
import reducer from './Reducer'
const initValues = { 'First': 0, 'Second': 10, 'Third': 20}
const store = createStore(reducer, initValues)
export default store
在Reducer.js中,通过处理派发出来的action,动态的修改目标数据,并返回一个新的对象,需要注意的是,Redux 中把存储state 的工作抽取出来交给Redux 框架本身, 让reducer 只用关心如何更新state , 而不要管state 怎么存,所以每次修改后都需要合并之前的state后返回,保证数据的一致性。redeucer函数接收两个参数,第一个为state,即store中的旧的状态,第二个参数action是派出出来的对象,上面会携带想做的操作类型和所携带的数据;
import * as ActionTypes from './ActionsTypes'
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
}
}
最后是Actions和ActionTypes,我们把ActionTypes抽出来单独写,可以更好的复用代码以及增加代码的可读性,每个action都返回一个对象,对象有个名为type的参数,存放action类型,其他字段为需要修改的参数;
ActionTypes.js
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
Actions.js
import * as ActionTypes from './ActionsTypes'
export const increment = (counterCaption) => {
return {
type: ActionTypes.INCREMENT,
counterCaption: counterCaption
}
}
export const decrement = (counterCaption) => {
return {
type: ActionTypes.DECREMENT,
counterCaption: counterCaption
}
}
最后来实现一下在组件中怎么去引用我们所创建的store,先上代码:
ControlPanel.js
import React, {Component} from 'react'
import Counter from './Counter'
import Summary from './Summary'
const style = { margin: '20px'};
class ControlPanel extends Component {
render() {
return (
<div style={style}>
<Counter caption='First' />
<Counter caption='Second' />
<Counter caption='Third' />
<hr/>
<Summary />
</div>
)
}
}
export default ControlPanel
Summary.js
import React, {Component} from 'react'
import store from '../Store'
class Summary extends Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this)
this.state = this.getOwnState()
}
onChange() {
this.setState(this.getOwnState())
}
getOwnState() {
const state = store.getState()
let sum = 0
for (const key in state) {
if (state.hasOwnProperty(key)) {
sum += state[key]
}
}
return {sum: sum}
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
return nextState.sum !== this.state.sum
}
componentDidMount() {
store.subscribe(this.onChange)
}
componentWillUnmount() {
store.unsubscribe(this.onChange)
}
render() {
const sum = this.state.sum
return (
<div>Total: {sum}</div>
)
}
}
export default Summary;
Counter.js
import React, {Component} from 'react'
import PropTypes from 'prop-types'
import store from '../Store'
import * as Actions from '../Actions'
const buttonStyle = { margin: '10px'};
class Counter extends Component {
constructor(props) {
super(props)
this.onIncrement = this.onIncrement.bind(this)
this.onDecrement = this.onDecrement.bind(this)
this.onChange = this.onChange.bind(this)
this.getOwnState = this.getOwnState.bind(this)
this.state = this.getOwnState()
}
getOwnState() {
return {
value: store.getState()[this.props.caption]
}
}
onIncrement() {
store.dispatch(Actions.increment(this.props.caption))
}
onDecrement() {
store.dispatch(Actions.decrement(this.props.caption))
}
onChange() {
this.setState(this.getOwnState())
}
shouldComponentUpdate(nextProps, nextState, nextContext) {
return (nextProps.caption !== this.props.caption)
|| (nextState.value !== this.state.value)
}
componentDidMount() {
store.subscribe(this.onChange)
}
componentWillUnmount() {
store.unsubscribe(this.onChange)
}
render() {
const value = this.state.value;
const {caption} = this.props;
return (
<div>
<button style={buttonStyle} onClick={this.onIncrement}>+</button>
<button style={buttonStyle} onClick={this.onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
)
}
}
Counter.propTypes = {
caption: PropTypes.string.isRequired
}
export default Counter
从代码中可以看出,当需要获取store中的状态的时候,可以通过store.getState()来获取store中state状态值,并在constructor中对this.state做初始化赋值,这样组件就能获取到初始数据;当需要派发一个action的时候,可以调用store.dispatch()来派发一个action,参数为导入的Actions.js文件中export的对象;我们还需要保持store和this.state的同步,在componentDidMount函数中,通过Store的subscribe监听其变化,只要Store状态发生变化,就会调用这个onChange方法;在componentWillUnmount函数中,需要把这个监听注销掉,防止内存泄漏;到这里,就简单实现了通过redux来共享数据,操作后效果如下:
四、改进React
通过上面的例子,我们可以发现一个规律,在Redux框架下,一个React组件基本上是完成以下两个功能:
- 和Redux打交道,读取Store中的状态,用于初始化组件的状态;同时还要监听Store的状态的改变,当Store中状态发生变化的时候,需要更新组件的状态,从而驱动组件重新渲染,当需要更新Store,就要派发action;
- 根据当前的state和props渲染组件
根据组件拆分的原则,一个组件只负责一件事情,所以可以考虑,把例子上的组件再拆分成两个组件,分别承担一个任务,然后把两个组件嵌套起来,完成原本一个组件完成的所有任务;在这样的关系下,两个组件是父子组件的关系。在业界中,承担第一个任务的组件,也是负责和Redux Store打交道的组件,处于外层,所以被叫做容器组件(聪明组件),对于承担第二个任务的组件,也是只负责渲染界面的组件,处于内层,叫做展示组件(傻瓜组件),它是一个纯函数。关系图如下,容器组件负责和Store打交道,获取数据后,通过props传给展示组件,展示组件再渲染出对应的界面;
我们可以对上面的例子中的Counter组件进行拆解分析,把原有的Counter拆分为两个组件,分别为展示组件Counter和容器组件CounterContainer;展示组件Counter就会变得很简单了,只需要接收props并将之渲染出来即可;
calss Counter extends Component {
constructor(props) {
super(props)
}
render(
const {caption, onlncrement , onDecrement , value) = this.props;
return (
<div>
<button style=(buttonStyle) onClick={onincrement)>+</button>
<button style={buttonStyle) onClick={onDecrement)>-</button>
<spa n>{caption} count : (value}</span>
</div>
)
)
}
对于无状态组件,可以进一步缩减代码,React支持只用一个函数表示的无状态组件,所以可以进行进一步缩减;
function Counter (props) {
const {caption,onincrement, onDecrement, value} = props;
return (
<div>
<button style=(buttonStyle) onClick={onincrement)>+</button>
<button style={buttonStyle) onClick={onDecrement)>-</button>
<spa n>{caption} count : (value}</span>
</div>
)
}
对于这种写法,获取props的值的方式不再是通过this.props来获取了,而是通过参数props获取,还有一种常用写法,就是把props的结构赋值直接放在参数中,可以再节省一行的代码量;
function Counter ({caption, onincrement, onDecrement , value} {
...
}
而对于容器组件CounterContainer,前面部分基本保留原有的Counter的方法声明和生命周期的声明,主要修改的是render函数返回的渲染内容;
class CounterContainer extends Component {
......
render(
return <Counter caption={this.props.caption}
onincrement={this.onincrement}
onDecrement={this.onDecrement}
value={this . state .value} />
)
}
接下来,我们需要再研究另外一个问题,就是现在都是哪里使用到Redux Store就直接导入Store,这样直接导入迟早会有问题;像在实际开发中,可能会通过npm引入第三方组件库,当开发一个独立的系统的时候,我们都不知道这个组件会在哪个位置,当然不可能知道预先定义唯一的Redux Store的文件位置了,所以直接导入Store是非常不利于组件的复用的;React提供了一个叫做Context的功能,能完美解决这个问题。
所谓Context,就是上下文环境,让一个树状组件上有一个所有组件都能够访问的对象,为了完成这个任务,需要上下级组件的配合。这个上级组件之下的所有子组件,只要宣称自己需要这个context,就可以通过this.context访问到这个共同的环境对象;所以需要创建一个拥有store的顶层组件,他是一个通用的context提供者,可以在其下的所有子组件中访问到context;我们暂时把这个组件称为Provider;
class Provider extends Component {
getChildContext () {
return {
store: this.props.store
}
}
render () {
return this.props.children
}
}
Provider的作用就是把子组件给渲染出来,在渲染中,Porvider 不做任何的处理;this.props.children是指两个标签之前的子组件,比如<Provider><ControlPanel /></Provider>,this.props.children指的就是<ConrolPanel />;除了把渲染工作交给子组件,Provider还提供了一个函数getChildContext,这个函数返回的就是代表Context的对象。为了让React认可Provider为一个Context的提供者,还需要指定Provider的childContextTypes属性,代码如下:
Provider.childContextTypes = {
store: PropTypes.object
}
Provider 还需要定义类的childContextTypes ,必须和getChildContext 对应,只有这两者都齐备, Provider 的子组件才有可能访问到context 。
import store from ’ ./Store .js ’
import Provider from ’. /Provider . js’
ReactDOM . render(
<Provider store={store }>
<ControlPanel />
</Provider> ,
document . getElementByid ( ’ root ’)
)
为了让CounterContainer能够访问到context,必须给CounterContainer类的ContextTypes赋值和Provider.childContextTypes一样的值两者必须一致,不然访问不到context,代码如下:
CounterContainer.contextTypes ={
store: PropTypes.object
}
在CounterContainer 中,所有对store的访问,都是通过this.context.store完成的,因为this.context就是Provider提供的context对象,所以getOwnState函数代码如下:
getOwnState () {
return {
value: this.context.store.getState()[this.props.caption]
}
}
最后,因为我们是自己定义构造函数的,通过this.context访问上下文,所以constructor中需要多接收一个参数
constructor (props, context) {
super(props, context)
}
这里有个小技巧,可以一劳永逸的解决参数个数问题,不需要因为每次参数个数不同而多次修改代码,就是利用arguments和...展开运算符,如下:
constructor () {
super(...arguments)
}
五、React-Redux
至此,上面已经讲解了两个可以改进React 一次来适应Redux 的方法,第一是把一个组件拆分为容器组件和傻瓜组件,第二是使用React 的Context 来提供一个所有组件都可以直接访问的Context ,也不难发现,这两种方法都有套路,完全可以把套路部分抽取出来复用,这样每个组件的开发只需要关注于不同的部分就可以了。
实际上,已经有这样的一个库来完成这些工作了,这个库就是react-redux。
需要使用npm install react-redux --save
安装react-redux库,安装完成后,需要做对一下三个文件做修改,首先是index.js文件,代码如下
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux'
import ControlPanel from './views/ControlPanel'
import store from './Store'import './index.css';
ReactDOM.render(
<Provider store={store}>
<ControlPanel/>
</Provider>,
document.getElementById('root')
);
我们需要在index中引入Provider,作为context的提供者,并将store作为props传进去,这里的思路就跟改进react的第二种方法一样;
在具体的组件中,需要怎么样去获取到store中的数据,下面以counter为例讲解一下;
import React from 'react'
import PropTypes from 'prop-types'
import * as Actions from '../Actions'
import {connect} from 'react-redux'
const buttonStyle = { margin: '10px'};
function Counter({caption, onIncrement, onDecrement, value}) {
return (
<div>
<button style={buttonStyle} onClick={onIncrement}>+</button>
<button style={buttonStyle} onClick={onDecrement}>-</button>
<span>{caption} count: {value}</span>
</div>
)
}
Counter.propTypes = {
caption: PropTypes.string.isRequired,
onIncrement: PropTypes.func.isRequired,
onDecrement: PropTypes.func.isRequired,
value: PropTypes.number.isRequired
}
function mapStateToProps(state, ownProps) {
return {
value: state[ownProps.caption]
}
}
function mapDispatchToProps(dispatch, ownProps) {
return {
onIncrement: () => {
dispatch(Actions.increment(ownProps.caption))
},
onDecrement: () => {
dispatch(Actions.decrement(ownProps.caption))
}
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Counter);
这里主要的修改是引入和使用了connect组件,connect是react-redux提供的一个函数,这个方法接收两个参数,mapStateToProps和mapDispatchToProps,执行结果依旧是一个函数,所以后面才继续跟着一个括号和参数,实际上这里就是后面会学习到的高阶组件;这里两次函数执行,第一次是connect函数的执行,第二次是把connect函数返回的函数再次执行,最后产生的就是容器组件,相当于前面所讲的CounterContainer;connect的具体工作就是把Store上的状态转化为内层傻瓜组件的prop,把内层傻瓜组件中用户动作转化为派送给Store的动作;对于例子中的mapStateToProps和mapDispatchToProps函数,名称是可以随便起的,只不过此处是用了业界习惯用法,这两个函数都可以包含第二个参数,代表的是ownProps,也就是直接传递给外层容器组件的props;
总结
Redux 是F lux 框架的一个巨大改进,Redux强调单一的数据源,保持状态只读和数据改变只能通过纯函数完成的原则,和React的UI=render(state)的思想完美契合。在这一块学习中,利用Counter循循渐进,为了就是更清晰的理解每个改动背后的动因,最后,我们终于通react-redux 完成了React 和Redux 的融合。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。