Redux入門篇

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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章