Redux是用來管理JavaScript應用的狀態的。官方文檔地址:Redux。本文講述如何在React中使用Redux,重點偏向Redux。若是須要了解React,能夠參考React官網或者React入門(前一陣入門React時寫的文|ω・))。javascript
Redux是什麼?java
Redux is a predictable state container for JavaScript apps.react
Redux是一個用在JavaScript應用中的可預測狀態容器(這裏的狀態指的是存放信息的對象)。Redux是用來對狀態進行統一管理的,整個應用的狀態以一棵對象樹的方式被存放在store中。狀態是隻讀的,惟一改變狀態的方式是dispatch actions,因此狀態的改變是可預測的。經過dispatch改變store中的state,經過subscribe監聽狀態的改變,經過getState來拿到當前的狀態,這樣數據的處理都集中在Redux中。git
爲何要用Redux?github
想象這樣的場景,組件一、組件二、組件三、組件五、組件7(下圖標綠的組件)中都須要使用一些數據,由於在React中,數據是單向流動的,因此在組件7中改變數據了,就須要組件7通知組件4改變數據,組件4再通知組件1改變數。在組件1中改變數據以後再層層往下傳遞數據。這還只是下圖這樣結構比較簡單的應用,若是應用更復雜,那處理起來就是一團亂麻了。因此咱們須要用Redux來對數據(狀態)進行統一的管理,當須要改變某數據時,直接改變store中的數據,當要獲取數據時,也直接從store中拿數據。json
React和Redux有什麼關係?redux
聽見Redux這個名字的時候,由於和React同樣都是以Re開頭的,可能會產生Redux是專門爲React解決問題的一個工具這樣的誤解,但實際上React和Redux啥關係也沒有,是兩個獨立的工具。你能夠將Redux和React結合起來使用,也能夠將Redux和jQuery,甚至Redux和JavaScript結合使用。app
Redux中有幾個必須知道的概念:action、reducer、store。下文中提到的state(狀態)指的是存放信息的對象,這個對象是放在Redux的store中的。dom
actions是從應用到store的數據的載體。當咱們須要改變store中的數據的時候,須要dispatch actions。action必須有一個type屬性,代表即將執行的action的類型,type一般被定義爲一個字符常量。action對象中的其餘屬性是攜帶的信息。異步
例如:
{
type: ADD_NUMBER,
number: 1
}
複製代碼
action creator是建立action的函數,簡單地返回action,它能根據傳入的參數設置action的屬性值。
例如:
const ADD_NUMBER = 'ADD_NUMBER'
export const addNumber = number => ({
type: ADD_NUMBER,
number
})
複製代碼
reducer是純函數,它是用來指明當action發送(dispatch)到store的時候,state是如何改變的。它的第一個參數爲前一個state,第二個參數爲action對象,返回的值是下一個state。
純函數:
- 傳入相同的參數會返回相同的結果。
- 執行純函數不會形成反作用。(這裏的反作用指的是函數改變了做用域以外的狀態值,好比函數外部定義了變量a,在函數內部改變了變量a的值。)
在reducer中不能作如下操做:改變它的參數;形成反作用;調用非純函數(好比Math.random()
)。
如下的caculate函數是一個簡單的reducer。若是createStore函數沒有傳入第二個參數(state的初始值),那麼首次調用caculate的時候,實參state的值爲undefined。因此給state參數設置了一個默認值0。
function caculate (state = 0, action) {
const { number } = action
switch (action.type) {
case 'ADD_NUMBER':
return state + number
case 'SUBTRACT_NUMBER':
return state - number
case 'MULTIPLY_NUMBER':
return state * number
default:
return state
}
}
複製代碼
當應用比較大的時候,通常的作法是使用多個小的reducers,每一個reducer處理特定的內容。而後使用Redux提供的combineReducers
將reducers合併爲一個reducer,合併後的reducer做爲createStore的參數來建立store實例。下文介紹完store以後,會給出一個簡單的例子來講明這部份內容。
store將actions和reducers結合起來,store的功能是:
getState()
拿到state。dispatch(action)
更新state。subscribe(listener)
註冊監聽器。listener是一個函數,當發送action的時候會執行listener。subscribe(listener)
會返回 一個函數,調用這個函數可以註銷監聽器。一個應用中只能有一個store,想要拆分數據的話,須要使用多個reducers。
如下代碼中的caculate是上文建立好的reducer,addNumber是上文定義的action creator。
import { createStore } from 'redux'
const store = createStore(caculate)
const listener = () => console.log(store.getState()) // 打印當前狀態
const unsubscribe = store.subscribe(listener)
store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))
unsubscribe()
複製代碼
使用store.subscribe(listener)註冊了監聽器,當store.dispatch()發送action到store,reducer將state改變以後,監聽器函數listener會執行,打印出如下結果:
1
3
6
複製代碼
我須要另外一個reducer來處理標籤切換,如下是相應的action creatore和reducer。
const SELECT_TAB = 'SELECT_TAB'
const selectTab = tab => ({
type: SELECT_TAB,
tab
})
function switchTab (state = 'CACULATE', action) {
switch (action.type) {
case 'SELECT_TAB':
return action.tab
default:
return state
}
}
複製代碼
如今就有了兩個reducer。使用combineReducers將reducers合併爲一個根reducer,將根reducer做爲createStore的第一個參數,初始的state值做爲createStore的第二個參數來建立store。
let state = {
caculate: 0,
switchTab: 'CACULATE'
}
const reducer = combineReducers({ caculate, switchTab })
const store = createStore(reducer, state)
const listener = () => console.log(store.getState())
const unsubscribe = store.subscribe(listener)
store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))
unsubscribe()
複製代碼
combineReducers所作的事情是生成一個函數,調用這個函數時,會執行每一個reducer,並將執行的結果從新整合爲一個對象,做爲新的state。當調用store.dispatch(addNumber(1))
時,兩個reducer(caculate和switchTab)都會被調用,它們執行的結果會組成一個新的state樹。
代碼段中的:
const reducer = combineReducers({ caculate, switchTab })
複製代碼
和如下代碼段的效果是同樣的,
function reducer (state = {}, action) {
return {
caculate: caculate(state.caculate, action),
switchTab: switchTab(state.switchTab, action)
}
}
複製代碼
只是combineReducers會有更多的處理操做。
react-redux插件是專門用於在React中使用redux的,react-redux官網地址。
React Redux將展現組件(presentational components)和容器組件(container components)分離了。
Redux官網中有二者的對比表格:
Presentational Components | Container Components | |
---|---|---|
Purpose | How things look (markup, styles) | How things work (data fetching, state updates) |
Aware of Redux | No | Yes |
To read data | Read data from props | Subscribe to Redux state |
To change data | Invoke callbacks from props | Dispatch Redux actions |
Are written | By hand | Usually generated by React Redux |
容器組件能將展現組件和Redux聯繫起來。通常使用React Redux提供的connect方法生成容器組件。
connect 方法
調用connect方法會返回一個包裝函數,這個函數接收組件並返回一個新的包裝組件。這裏的包裝組件便是上文說到的容器組件,容器組件能將展現組件和Redux聯繫起來。
connect的使用方法是:
connect(mapStateToProps? mapDispatchToProps?, mergeProps?, options?)(組件)
複製代碼
Provider組件
全部的容器組件都須要可以訪問Redux的store,若是不使用Provider組件,就只能在全部容器組件的屬性中傳入store,假如在組件樹中深層嵌套了容器組件,可能有的展現組件也須要傳入store屬性。可是使用Provider組件包裹上應用根組件後,應用中的全部容器組件就都能訪問到Redux的store了。
舉個例子,定義容器組件的時候這樣寫:
import { connect } from 'react-redux'
import { addNumber, subtractNumber, multiplyNumber } from '../actions'
import Caculation from '../components/Caculation'
const mapStateToProps = state => ({
caculate: state.caculate
})
const mapDispatchToProps = dispatch => ({
plus: number => dispatch(addNumber(number)),
subtract: number => dispatch(subtractNumber(number)),
multiply: number => dispatch(multiplyNumber(number))
})
export default connect(
mapStateToProps,
mapDispatchToProps
)(Caculation)
複製代碼
那麼展現組件Caculation的屬性中就會有caculate,plus,subtract,multiply這幾個屬性,其中caculate是state.caculate;plus,subtract,multiply是三個函數,調用函數的時候能進行dispatch操做。
Redux只支持同步數據流,若是想要使用異步actions,須要使用applyMiddleware加強createStore。使用redux-thunk中間件可以發起同步或異步的actions,而且能夠設計知足某種條件才dispatch actions。使用Redux Thunk以後,action creator能夠返回一個函數,這個函數的第一個參數是dispatch,第二個參數是getState。 首先下載好redux-thunk,並使用applyMiddleware加強createStore:
import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
...
const store = createStore(rootReducer, applyMiddleware(thunk))
複製代碼
在thunk中間件的幫助下,就能夠像下面這樣使用異步action 了。在請求開始和請求結束的地方都觸發一個action,在請求開始的地方觸發的action能夠用來展現一個加載中的loading圖等。這裏只簡單地在請求開始時觸發了action,沒有作後續的使用。
const requestLeaderBoard = () => ({
type: REQUEST_LEADERBOARD
})
const receiveLeaderBoard = (json) => ({
type: RECEIVE_LEADERBOARD,
leaderboard: json.data
})
export function fetchLeaderBoard () {
return dispatch => {
dispatch(requestLeaderBoard())
return fetch(`這裏用一個get請求的地址`)
.then(res => res.json())
.then(json => dispatch(receiveLeaderBoard(json)))
}
}
複製代碼
爲了學習redux寫了一個練習項目:源碼地址
定義的reducer執行了「多餘」的次數。
@@redux/INITd.c.p.m.e.7
。@@redux/INITf.1.c.7.u.x
、@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9
、@@redux/INITf.1.c.7.u.x
。有3個多出的reducer調用。狀況2的問題請教了可愛的同事楊老闆,楊老闆找到了相應的源碼給出瞭解答,而後我又按照相同的方式在源碼中找了下,獲得了狀況1的答案。
(1)狀況1出現的緣由:
當使用createStore建立好一個store以後,Redux會dispatch一個初始化action(以下),以保證每一個reducer返回它們的默認state。
dispatch({ type: ActionTypes.INIT })
複製代碼
(2)狀況2出現的緣由:
執行第一個action@@redux/INITf.1.c.7.u.x
是由於Redux在執行combineReducers的過程當中調用了一遍reducer,做用是檢查調用reducer返回的初始state是否是undefined,若是是undefined會拋出一個錯誤。
const initialState = reducer(undefined, { type: ActionTypes.INIT })
複製代碼
第二個action@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9
也是在combineReducers執行的過程當中進行處理的,目的是檢查action的名稱和Redux內私有的名稱是否重複,若是重複,就拋出一個錯誤。
第三個action和狀況1同。