Redux入门篇

1、redux解决问题

react在使用的过程当中,主要是利用组件内的state来存储状态,在多个组件共享数据的时候,每一个组件内都得保存相同一份数据,会形成数据重复冗余;而利用props进行组件间的通信,在组件比较简单的时候,该方法倒没什么不妥,但随着咱们的应用愈来愈大,愈来愈复杂,单纯的靠props进行组件间的通信,会增长代码的复杂度和可读性,但查询数据源bug的时候,会变得极其复杂;更严格的数据流控制,是解决这个问题的所在。css

2、Redux的基本原则

  • 惟一数据源
  • 保持状态只读
  • 数据改变只能经过纯函数完成

让咱们逐一解释一下三个原则。node

1. 惟一数据源

惟一数据源是指应用的状态数据只存在惟一的Store上,若是数据存在多个store上,容易形成数据冗余,并且也容易致使数据一致性方面出现问题,并且,多个Store上面的数据若是存在依赖性,会增长应用的复杂程度,容易带来新的问题;固然,Redux并无阻止一个应用拥有多个store,但这样不只没有任何好处,甚至还不如一个store更容易组织代码。
这个惟一Store上的状态就是一个树形的形象,每一个组件上每每只是树形对象上的一部分,而如何设计Store上的树形对象,就是Redux中的核心问题。react

2.保持状态只读

要驱动用户界面渲染,就要改变应用的状态,保持状态只读,就是说不能直接去修改状态,要修改Store上的状态,须要经过派发一个action去处理;action会建立一个新的状态对象返回给Redux,有Redux完成新的状态的组装。npm

3.数据改变只能经过纯函数完成

所谓纯函数,就是不对外界产生任何反作用的函数;这里所说的纯函数,就是Reducer;Reducer并非Redux的特有术语,是计算机的一个通用概念,就比如是JavaScript中的reduce(fn,init)函数,里面接收的回调函数fn就是一个Reducer;
在Redux中,每一个reducer的函数签名以下所示:redux

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只负责计算状态,不负责保存状态。服务器

3、Redux实例

为了方便,直接用create-react-app工具来初始化项目,执行下面指令前,必须保证咱们的电脑已经安装了node.js;数据结构

npm install --global create-react-app

在命令行窗口中执行以上语句,安装create-react-app工具,安装成功后,能够获得如截图的内容;
Image.png
接下来咱们在命令行执行下面的指令,建立测试使用的应用;架构

create-react-app react-redux-app

建立成功后,进入项目目录,启动应用;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

4、改进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)
}

5、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 的融合。

相关文章
相关标签/搜索