react在使用的過程當中,主要是利用組件內的state來存儲狀態,在多個組件共享數據的時候,每一個組件內都得保存相同一份數據,會形成數據重複冗餘;而利用props進行組件間的通信,在組件比較簡單的時候,該方法倒沒什麼不妥,但隨着咱們的應用愈來愈大,愈來愈複雜,單純的靠props進行組件間的通信,會增長代碼的複雜度和可讀性,但查詢數據源bug的時候,會變得極其複雜;更嚴格的數據流控制,是解決這個問題的所在。css
讓咱們逐一解釋一下三個原則。node
惟一數據源是指應用的狀態數據只存在惟一的Store上,若是數據存在多個store上,容易形成數據冗餘,並且也容易致使數據一致性方面出現問題,並且,多個Store上面的數據若是存在依賴性,會增長應用的複雜程度,容易帶來新的問題;固然,Redux並無阻止一個應用擁有多個store,但這樣不只沒有任何好處,甚至還不如一個store更容易組織代碼。
這個惟一Store上的狀態就是一個樹形的形象,每一個組件上每每只是樹形對象上的一部分,而如何設計Store上的樹形對象,就是Redux中的核心問題。react
要驅動用戶界面渲染,就要改變應用的狀態,保持狀態只讀,就是說不能直接去修改狀態,要修改Store上的狀態,須要經過派發一個action去處理;action會建立一個新的狀態對象返回給Redux,有Redux完成新的狀態的組裝。npm
所謂純函數,就是不對外界產生任何反作用的函數;這裏所說的純函數,就是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只負責計算狀態,不負責保存狀態。服務器
爲了方便,直接用create-react-app工具來初始化項目,執行下面指令前,必須保證咱們的電腦已經安裝了node.js;數據結構
npm install --global create-react-app
在命令行窗口中執行以上語句,安裝create-react-app工具,安裝成功後,能夠獲得如截圖的內容;
接下來咱們在命令行執行下面的指令,建立測試使用的應用;架構
create-react-app react-redux-app
建立成功後,進入項目目錄,啓動應用;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來共享數據,操做後效果以下:
經過上面的例子,咱們能夠發現一個規律,在Redux框架下,一個React組件基本上是完成如下兩個功能:
根據組件拆分的原則,一個組件只負責一件事情,因此能夠考慮,把例子上的組件再拆分紅兩個組件,分別承擔一個任務,而後把兩個組件嵌套起來,完成本來一個組件完成的全部任務;在這樣的關係下,兩個組件是父子組件的關係。在業界中,承擔第一個任務的組件,也是負責和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 的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 的融合。