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

相關文章
相關標籤/搜索