1

一、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工具,安装成功后,可以得到如截图的内容;
Image.png
接下来我们在命令行执行下面的指令,创建测试使用的应用;

create-react-app react-redux-app

创建成功后,进入项目目录,启动应用;

cd react-redux-app
npm start

这个命令启动一个开发者模式的服务器,同时也会让你的浏览器自动打开一个网页,指向本机http://localhost:3000/
create-react-app指令安装成功截图:
Image [2].png
启动应用成功截图:
Image [3].png
应用启动后的初始界面:
Image [4].png
因为个人习惯,一般我都会执行 npm run eject,该指令的作用是,就是把潜藏在react-scripts 中的一系列技术找配置都“弹射”到应用的顶层,然后就可以研究这些配置细节了,而且可以更灵活地定制应用的配置。在react和redux结合使用的时候,没有理由不选择使用react-redux库,这样能大大节省代码的书写,不过从一开始我们不直接使用它,不然会对其内部设计一头雾水,所以先从最简单的redux开始使用,一步步改进,循循渐进地过度到react-redux。下面是项目目录结构:
Image [5].png
其中,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来共享数据,操作后效果如下:
Image [6].png
Image [7].png

四、改进React

通过上面的例子,我们可以发现一个规律,在Redux框架下,一个React组件基本上是完成以下两个功能:

  • 和Redux打交道,读取Store中的状态,用于初始化组件的状态;同时还要监听Store的状态的改变,当Store中状态发生变化的时候,需要更新组件的状态,从而驱动组件重新渲染,当需要更新Store,就要派发action;
  • 根据当前的state和props渲染组件

根据组件拆分的原则,一个组件只负责一件事情,所以可以考虑,把例子上的组件再拆分成两个组件,分别承担一个任务,然后把两个组件嵌套起来,完成原本一个组件完成的所有任务;在这样的关系下,两个组件是父子组件的关系。在业界中,承担第一个任务的组件,也是负责和Redux Store打交道的组件,处于外层,所以被叫做容器组件(聪明组件),对于承担第二个任务的组件,也是只负责渲染界面的组件,处于内层,叫做展示组件(傻瓜组件),它是一个纯函数。关系图如下,容器组件负责和Store打交道,获取数据后,通过props传给展示组件,展示组件再渲染出对应的界面;
Image [8].png
我们可以对上面的例子中的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的功能,能完美解决这个问题。
Image [9].png
所谓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 的融合。


煤气没气了
88 声望7 粉丝