react之redux基本用法
在react中,元件與元件進行資料互動時,我們可以用this.props在不同元件之間傳遞資料,但是當開發一個大型應用時,用this.props將資料一層一層的傳下去來進行元件之間的通訊就會變得非常的麻煩,這個時候,redux應運而生,redux就是用來解決開發龐大應用時的資料互動問題。
但是,redux並不適用與所有場景,如果元件之間沒有大量資料互動,元件不需要共享狀態,或者應用比較簡單的話,redux的加入反而會增加程式碼的複雜性。多互動,元件需要共享狀態,才是redux的適用場景。
redux的幾個基本概念
一. state
state是redux儲存資料的地方,並且state只能有一個。有的地方講store是儲存資料的地方,但是我更傾向於將store理解成一個裝著獲取state(getState)、操作state(dispatch)和監聽state(subscribe)的方法
redux規定,一個state對應一個view,知道state就知道view,知道view就知道state,state驅動著頁面(view),這點跟react的思想也是一樣的,即:資料驅動頁面。
二. action
我們是不能直接操作state的,我們只能接觸到view,action就是操作view後,view發出的通知,告訴state要改變了,具體怎麼改變取決於action的內容。
action是一個物件,它必須要含有一個type屬性,用來區別不同型別的action,下面是一個action的例子:
其他的屬性可以根據自己的需要來定義。三. reducer
reducer是一個函式,它接受2個引數,一個是state,一個是action,然後根據action的內容對state作出修改,並返回一個新的state,下面是一個例子:
注意:引數state有一個預設值,這個預設值會作為state的初始值(等下會解釋為什麼).
reducer必須是一個純函式,所謂純函式就是,相同的輸入必定得到相同的輸出,就像高中數學的函式一樣,一個x只能對應一個y,如果一個x對應2個y那麼就不能稱之為函式。
reducer函式不用直接使用,它會在dispatch函式中呼叫。
四. store
講了這麼多,終於要講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決定了view
- 操作view發出action
- reducer函式接受action後修改state 這裡有張圖:
可以看到,redux的流程是單向的。
六. combineReducers
開發大型專案的時候,一個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沒有任何區別。
redux的使用
上面我們已經生成了store,接下來的問題就是:
- 怎麼在react專案的各個地方都能拿到store。
- 拿到之後怎麼對store進行操作。
這裡我要講一個別人寫好的庫:react-redux,這個庫使用起來比直接使用redux方便,但是它有一些自己的規範,我們要按照它的規範來使用。
一. Provider元件
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了。
二. connect
我們使用了Provider元件將store"全域性化"之後,現在就要開始使用store了,怎麼使用呢?直接store.getState(),或者store.dispatch()嗎?肯定沒這麼簡單,直接這樣用react會給你報錯的。
2.1 connect的使用
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包起來,不然不讓你用。
2.2 connect函式的引數
上面我們看到了store與元件是如何聯絡起來的,connect發揮了它的作用,但是,僅僅聯絡起來是不夠的,我們還沒有定義操作邏輯,即:如何對state進行操作?事實上上面的connect函式我們沒有寫完,它是有2個引數的:mapStateToProps和mapDispatchToProps。它的完整寫法應該是:
export default connect(mapStateToProps,mapDispatchToProps)(Header)
複製程式碼
2.3 mapStateToProps
這個函式的名字取得非常的語義化,將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的響應式的特點相同。
2.4 mapDispatchToProps
這個函式跟mapStateToProps是一樣的,不過這次對映的是dispatch,之前對映的是state,讓我們來直接看看它是怎麼寫的吧:
mapStateToProps拿到的是state,而mapDispatchToProps拿到的是dispatch,它跟mapStateToProps一樣,依然需要返回一個物件,物件上面的每個屬性,都會被放到(對映)props上面,聽起來是不是跟mapStateToProps一模一樣!區別就在於mapDispatchToProps得到的是函式,返回的物件裡面的屬性也是函式,這些函式可以呼叫dispatch以修改state,在元件裡面我們又可以呼叫這些函式,這樣就完成了dispatch到props的對映。mapDispatchToProps定義了UI元件怎麼發出action。
mapDispatchToProps不一定要寫成函式,它也可以寫成物件的形式,這裡不做深究,知道就好了。
2.5 讓我們來一睹全貌
分析:(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。
2.6 UI元件與容器元件
上面講了react-redux的一些規則,它還有一些規則,UI元件與容器元件就是其中之一。
簡單的來說UI元件就是我們自己寫的元件,比如上面的Header,它負責UI的呈現,而容器元件負責資料管理和邏輯,它不用自己寫,connect函式幫我們生成了它。
最後export出去的,就是一個UI元件和容器元件的結合體。它包含著我們自己寫的負責視覺層(view)的Header,以及負責與store對接、進行資料互動、狀態管理的容器元件。
小結
一. 生成store
- reducer函式接受2個引數:state和action,返回新的state。
- createStore(reducer)生成store。
- store.dispatch(action)修改state。
- store.getState()獲取state。
- store.subscribe()監聽state。
二. store"全域性化"
- Provider包住根元件。
- 原理:context上下文物件。
三. 元件與store進行互動
- 將元件用connect包一層。
- mapStateToProps將state對映到props。
- mapDispatchToProps將dispatch對映到props。
- UI元件與容器元件。