在react中,組件與組件進行數據交互時,咱們能夠用this.props在不一樣組件之間傳遞數據,可是當開發一個大型應用時,用this.props將數據一層一層的傳下去來進行組件之間的通訊就會變得很是的麻煩,這個時候,redux應運而生,redux就是用來解決開發龐大應用時的數據交互問題。react
可是,redux並不適用與全部場景,若是組件之間沒有大量數據交互,組件不須要共享狀態,或者應用比較簡單的話,redux的加入反而會增長代碼的複雜性。多交互,組件須要共享狀態,纔是redux的適用場景。redux
state是redux保存數據的地方,而且state只能有一個。有的地方講store是保存數據的地方,可是我更傾向於將store理解成一個裝着獲取state(getState)、操做state(dispatch)和監聽state(subscribe)的方法的對象。具體什麼是store,且聽下文分解。數組
redux規定,一個state對應一個view,知道state就知道view,知道view就知道state,state驅動着頁面(view),這點跟react的思想也是同樣的,即:數據驅動頁面。閉包
咱們是不能直接操做state的,咱們只能接觸到view,action就是操做view後,view發出的通知,告訴state要改變了,具體怎麼改變取決於action的內容。ide
action是一個對象,它必需要含有一個type屬性,用來區別不一樣類型的action,下面是一個action的例子:函數
其餘的屬性能夠根據本身的須要來定義。reducer是一個函數,它接受2個參數,一個是state,一個是action,而後根據action的內容對state做出修改,並返回一個新的state,下面是一個例子:ui
注意:參數state有一個默認值,這個默認值會做爲state的初始值(等下會解釋爲何).this
reducer必須是一個純函數,所謂純函數就是,相同的輸入一定獲得相同的輸出,就像高中數學的函數同樣,一個x只能對應一個y,若是一個x對應2個y那麼就不能稱之爲函數。spa
reducer函數不用直接使用,它會在dispatch函數中調用。翻譯
講了這麼多,終於要講store了
store跟state同樣,只能有一個,咱們要引入redux中的createStore來建立store,以下:
import { createStore } from 'redux';
const store = createStore(fnc);
複製代碼
注意:createStore接受一個函數做爲參數
下面我引用了阮一峯老師的一篇文章中的一段:
上面是一個createStore函數的簡單實現。且聽我娓娓道來。首先,咱們能夠看到state的聲明,就是一普普統統的變量,再看getState函數,功能很簡單,就是直接返回state。
接下來是dispatch,它接受一個action做爲參數,而後在內部執行reducer函數,返回一個新的state,咱們先不看下面的 listeners.forEach(listener => listener()); reducer函數是經過createStore函數的參數傳遞進來的,而後在dispatch內部調用,dispatch是createStore函數的返回對象中的三個函數之一,爲何不直接將reducer暴露出去而是選擇將其間接在dispatch中調用而後將dispatch暴露出去呢,個人理解是:reducer要接受state做爲參數,若是直接將reducer暴露出去的話,那麼state勢必也要暴露出去,這樣的話redux的穩定性就會大幅下降,這種方法是不可取的,因此redux的做者選擇間接調用reducer,而不是直接將其暴露出去。 dispatch的意思是:派遣,迅速執行,dispatch函數顧名思義,就是獲得一個action指令,當即處理。
上面我講了,reducer函數的默認值會做爲state的初始值,爲何呢?能夠看到,createStore函數在return以前,調用了一次dispatch,傳了一個空對象,咱們進入函數內部來分析一下此次調用的過程,此次咱們依然忽略 listeners.forEach(listener => listener()); 這個語句,以下:
(1)state聲明時並無賦值,這個時候它是undefined。
(2) action爲{},它並無type值,這個時候reducer函數的調用至關於: reducer(undefined。 ,{}),形參state接受到了一個undefined,這個時候它就會使用默認值,也就是defaultState。
(3)由於action是空對象,沒有type值,那麼switch匹配不到相應的case,進入到default,返回原state對象,也就是defaultState。這就是爲何reducer函數的默認值會做爲state的初始值。
最後是subscribe函數,在createStore函數開頭除了聲明瞭state,還聲明瞭一個listeners數組,subscribe函數接受一個listener,listener是一個函數,它會被push到listeners數組裏面,上面被我忽略了2次的 listeners.forEach(listener => listener()); 終於要拿出來了,這個語句的做用就是將listeners數組裏面的每個函數,也就是listener都執行一次。那爲何這句話要放在dispatch中執行呢?顯然,listener的意思是監聽者,監聽誰?固然是state,而只有dispatch才能引發state的改變,因此要將這個語句放在dispatch函數裏面,每次dispatch後state會變化,同時執行全部listener函數。
subscribe翻譯過來是訂閱,咱們訂閱了state,每次state改變了,都會讓咱們收到消息並做出迴應(執行全部listener函數),就像咱們訂閱B站UP主同樣,UP主每次發佈新視頻,咱們都能獲得消息,而後本身選擇看或者不看。
這裏尚未結束,咱們能夠看到subscribe函數還返回了一個函數,這個函數執行後會將當前listener在listeners數組中移除掉,這個動做就是unsubscribe,也就是取消訂閱。取消訂閱後,state再更新,就不會執行相應的操做了。
來看個例子:
從上面那張圖來看,能夠發現,createStore的核心原理就是JS的閉包,將state在閉包中聲明,而後將獲取、修改、監聽state的三個函數放在一個對象裏面暴露出去,這個對象就是store。將store對象傳到不一樣的地方,就能在不一樣的地方共享同一個state。這就是redux想給咱們的東西了。
能夠看到,redux的流程是單向的。
開發大型項目的時候,一個state多是巨大的,若是隻用一個reducer來處理state的話,那麼這個reducer將是極其複雜的,這樣的話,是否是能夠經過一種方法,將reducer拆分紅多個小reducer,負責state不一樣的reducer區域呢?
Redux 提供了一個combineReducers方法,咱們從redux中引入它。以下:
import { combineReducers } from 'redux';
複製代碼
它怎麼用呢?下面是一個例子:
let rootReducer = combineReducers({
todos:todosReducer,
counter:counterReducer,
})
複製代碼
combineReducers函數接受一個對象,對象裏面是一些小的reducer函數,返回值是一個大的reducer,顧名思義,就是將reducer combine起來。
combineReducers函數使用了一些小技巧,讓咱們能夠分開寫reducer函數,以應對大型項目的龐大的state,可是它的基本原理仍是不變的,它依然是一個reducer函數,操做起來跟普通的reducer函數也是如出一轍的,須要接受2個參數,一個是state,一個是action,而且返回一個新的state。
下面來說一下它的原理,依然引用一下阮一峯老師的的內容。(他講的真的是太好了)
上面是combineReducers的簡單實現,它有一個reducers參數,跟上面講的同樣,這是一個對象,裏面包含着全部小的reducer。且聽我娓娓道來。執行combineReducers函數後,返回的是一個這樣的函數,我把它賦給rootReducer,並從上面那幅圖中分離出來,以便分析,以下:
rootReducer即是執行combineReducers函數後返回的函數了。就像上面講的,它依然是一個reducer,有一個state參數,一個action參數,而且返回一個新的state。咱們來分析一下它的返回值,我把它從圖中分離出來,以便分析。
如上圖,它是這樣一個對象,含義就是:給reducers對象中的全部小reducer(reducers[key])傳入對應部分的state(state[key])和action而後執行,返回值放入nextState對象的對應部分(nextState[key]),最後將這個nextState對象返回。這個nextState對象就是我所說的新的state了,只不過是這種方式看起來更復雜一點而已。注意,不要覺得傳入一個rootReducer傳入一個action只會在對應的小reducer的switch中判斷,事實是:它會在全部的小reducer中判斷,這也是爲何,我說combineReducers返回的大reducer跟普通的reducer沒有什麼區別的緣由之一。
我寫了3個reducer函數而後將其放到一個對象裏面做爲combineReducers函數的參數,請注意這3個小reducer的state參數的默認值,組合完後咱們來輸出一下state: 能夠看到,就如上面所說,小reducer函數中的state參數的默認值會看成對應的state區域的初始值,一個reducer對應state對象的一個屬性。 state對象中的屬性的屬性名與combineReducers函數參數中的幾個屬性的屬性名相同,他們是一一對應的(能夠看上面combineReducers函數的簡單實現的分析)。這樣一來,咱們就完成了分區域寫reducer,而不用寫在一塊兒。還有一個緣由,那就是小reducer函數中的state參數的默認值會看成對應的state區域的初始值,由於將大reducer函數最終仍是要看成createStore函數的參數,而後createStore函數內部會執行一次dispatch,給state初始化(能夠看上面store部分dispatch的解析),在這個過程當中,全部的小reducer都會被執行一次,並給各個對應的state區域初始化,最後就完成了state的初始化。來看個例子:
綜上所述,combineReducers函數讓咱們能夠將reducer拆開來寫,可是最後的reducer跟普通的reducer沒有任何區別。
上面咱們已經生成了store,接下來的問題就是:
- 怎麼在react項目的各個地方都能拿到store。
- 拿到以後怎麼對store進行操做。
這裏我要講一個別人寫好的庫:react-redux,這個庫使用起來比直接使用redux方便,可是它有一些本身的規範,咱們要按照它的規範來使用。
Provider組件讓store組件在任何位置都能被取到,它的原理是react的context上下文對象,這裏不作解釋。
那Provider組件怎麼使用呢?很簡單,先將它引入:
import { Provider } from 'react-redux'
複製代碼
而後將根組件用Provider包起來,像這樣:
render(
<Provider store={store}> <App /> </Provider>,
document.getElementById('root')
)
複製代碼
不必定要包着App組件,你也能夠:
class App extends React.Component {
render(){
return (
<Provider store={store}> <div className="App"> <Header /> <Content /> </div> </Provider>
);
}
}
複製代碼
反正要記住一點,只有被Provider包起來的組件,才能夠拿到store,因此咱們儘可能包住整個react項目的根組件。
別忘了那句store={store},這是將store傳下去的要點,通常咱們用createStore生成了store,而後將store賦值給同名變量store,這樣就能保證Provider裏面的組件能拿到store了。
咱們使用了Provider組件將store"全局化"以後,如今就要開始使用store了,怎麼使用呢?直接store.getState(),或者store.dispatch()嗎?確定沒這麼簡單,直接這樣用react會給你報錯的。
react-redux這個庫提供了connect方法來創建組件與store之間的通訊。要使用connect,首先咱們要引入它:
import { connect } from 'react-redux'
複製代碼
而後咱們要用connect將要輸出的組件包起來,也就是說,咱們不能直接export組件了,如今要export的是通過connect包裝事後的組件,咱們來看一下二者的區別:
假如咱們要輸出一個Header組件,大體是這樣:
class Header extents Component{
//.....
//...
}
export default Header
複製代碼
使用了connect後,要這樣:
class Header extents Component{
//.....
//...
}
export default connect()(Header)
複製代碼
看到區別了嗎,上面講了咱們react-redux這個庫有本身的規則,這就是它的規則之一,你這個組件想要用store,就必需要先用connect將其包起來,或者理解爲將其與store鏈接起來。connect就是一張通行證,想要用store是吧,先給我用connect包起來,否則不讓你用。
上面咱們看到了store與組件是如何聯繫起來的,connect發揮了它的做用,可是,僅僅聯繫起來是不夠的,咱們尚未定義操做邏輯,即:如何對state進行操做?事實上上面的connect函數咱們沒有寫完,它是有2個參數的:mapStateToProps和mapDispatchToProps。它的完整寫法應該是:
export default connect(mapStateToProps,mapDispatchToProps)(Header)
複製代碼
這個函數的名字取得很是的語義化,將state映射到props,它的寫法是這樣的:
react-redux會將state做爲參數傳給它,而後這個函數會返回一個對象,這個對象的屬性會被放到當前組件的props上面,而後咱們就能夠拿到咱們想要的屬性了,以上面的那張圖爲例,咱們能夠這樣使用props:class Header extents Component{
render(){
let lists = this.props.lists;
return(
//....
//....
)
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Header)
複製代碼
關於mapStateToProps,咱們要知道的是:
(1) 咱們能夠根據本身的須要隨便return什麼,mapStateToProps的功能就是將state映射到props上面,至於怎麼映射,取決於咱們本身,若是咱們這樣寫:
const mapStateToProps = (state) => {
return {
state : state,
lists : state.lists,
userName : state.userName,
}
}
複製代碼
那麼props上就會有state、lists、userName:
class Header extents Component{
render(){
let {state , lists , userName} = this.props;
return(
//....
//....
)
}
}
export default connect(mapStateToProps,mapDispatchToProps)(Header)
複製代碼
(2)mapStateToProps會訂閱store,一旦state發生改變,mapStateToProps會自動執行,從新計算UI組件的參數,觸發UI組件的從新渲染,這跟react的響應式的特色相同。
這個函數跟mapStateToProps是同樣的,不過此次映射的是dispatch,以前映射的是state,讓咱們來直接看看它是怎麼寫的吧:
mapStateToProps拿到的是state,而mapDispatchToProps拿到的是dispatch,它跟mapStateToProps同樣,依然須要返回一個對象,對象上面的每一個屬性,都會被放到(映射)props上面,聽起來是否是跟mapStateToProps如出一轍!區別就在於mapDispatchToProps獲得的是函數,返回的對象裏面的屬性也是函數,這些函數能夠調用dispatch以修改state,在組件裏面咱們又能夠調用這些函數,這樣就完成了dispatch到props的映射。mapDispatchToProps定義了UI組件怎麼發出action。
mapDispatchToProps不必定要寫成函數,它也能夠寫成對象的形式,這裏不作深究,知道就行了。
(1) 咱們從state裏面取到了lists,而後在render函數裏面將它用map函數轉化成一個li數組,再放在return裏面將其渲染出來。
(2)咱們定義了一個input標籤,它是一個受控組件,它收到this.state的控制(注意不要搞混了這幾個state)
(3)咱們定義了一個button,給它添加了一個onClick函數,在它的onClick函數中,調用了myOnClick函數,它會調用dispatch函數對state進行修改,react-redux監聽到了state的變化後,再次執行mapStateToProps,從新計算lists的值,並引發UI組件的從新渲染。這個時候,ul列表裏面會多一條li。
上面講了react-redux的一些規則,它還有一些規則,UI組件與容器組件就是其中之一。
簡單的來講UI組件就是咱們本身寫的組件,好比上面的Header,它負責UI的呈現,而容器組件負責數據管理和邏輯,它不用本身寫,connect函數幫咱們生成了它。
最後export出去的,就是一個UI組件和容器組件的結合體。它包含着咱們本身寫的負責視覺層(view)的Header,以及負責與store對接、進行數據交互、狀態管理的容器組件。