學習必備要點:
- 首先弄明白,Redux在使用React開發應用時,起到什么作用——狀態集中管理
- 弄清楚Redux是如何實現狀態管理的——store、action、reducer三個概念
- 在React中集成Redux:redux + react-redux(多了一個概念——selector)
- Redux調試工具:redux devtools
- redux相關很好用的插件:redux-saga的相關介紹
redux結構圖
其中紅色虛線部分為redux的內部集成,不能顯示的看到。
- action:是事件,它本質上是JavaScript的普通對象,它描述的是“發生了什么”。action由type:string和其他構成。
- reducer是一個監聽器,只有它可以改變狀態。是一個純函數,它不能修改state,所以必須是生成一個新的state。在default情況下,必須但會舊的state。
- store是一個類似數據庫的存儲(或者可以叫做狀態樹),需要設計自己的數據結構來在狀態樹中存儲自己的數據。
Redux入門
Redux簡介
Redux是一個狀態集中管理庫。
安裝
npm install --save redux
附加包
npm install --save react-redux
npm install --save-dev redux-devtools
三大原則
單一數據源
整個應用的state被存儲在一棵object tree中,并且這個object tree只存在于唯一一個store中。
State是只讀的
惟一改變 state 的方法就是觸發 action,action 是一個用于描述已發生事件的普通對象。
使用純函數來執行修改
為了描述action如何改變狀態樹,我們需要編寫reducers。Reducer只是一些純函數,他接受先前的state和action,并返回新的state對象。
上圖是Redux如何實現狀態管理的框架,View(視圖) 可以通過store.dispatch()方法傳遞action。 Action相當于事件模型中的事件,它描述發生了什么。Reducer相當于事件模型中的監聽器,它接收一個舊的狀態和一個action,從而處理state的更新邏輯,返回一個新的狀態,存儲到Store中。而從store-->view 的部分,則是通過mapStateToProps
這個函數來從Store中讀取狀態,然后通過props屬性的方式注入到展示組件中。圖中紅色虛線部分是Redux內部處理,我們不必過多考慮這部分的實現。
Action
Action 是把數據從應用傳到store的有效載荷,它是store數據的唯一來源,一般來說,我們通過store.dispatch()將action傳到store。
Action創建函數
Action 創建函數 就是生成 action 的方法。“action” 和 “action 創建函數” 這兩個概念很容易混在一起,使用時最好注意區分。
Redux中action創建函數只是簡單返回一個action。
改變userName的示例:
export function changeUserName(userName) { // action創建函數
return { // 返回一個action
type: 'CHANGE_USERNAME',
payload: userName,
};
}
Action 本質上是JavaScript 普通對象。我們規定,action 內必須使用一個字符串類型的 type
字段來表示將要執行的動作。多數情況下,type
會被定義成字符串常量。當應用規模越來越大時,建議使用單獨的模塊或文件來存放 action。
除了 type
字段外,action 對象的結構完全由你自己決定。參照 Flux 標準 Action 獲取關于如何構造 action 的建議,另外還需要注意的是,我們應該盡量減少在action中傳遞數據。
Reducer
Action只是描述有事情發生這一事實,而Reducer用來描述應用是如何更新state。
設計State結構
在 Redux 應用中,所有的 state 都被保存在一個單一對象中。在寫代碼之前我們首先要想清楚這個對象的結構,要用最簡單的形式把應用中的state用對象描述出來。
HelloApp應用的state結構很簡單,只需要保存userName即可:
{userName: 'World'}
處理 Reducer 關系時的注意事項
開發復雜的應用時,不可避免會有一些數據相互引用。建議你盡可能地把 state 范式化,不存在嵌套。把所有數據放到一個對象里,每個數據以 ID 為主鍵,不同實體或列表間通過 ID 相互引用數據。把應用的 state 想像成數據庫。這種方法在 normalizr 文檔里有詳細闡述
Action處理
確定了 state 對象的結構,就可以開始開發 reducer。reducer 就是一個純函數,接收舊的 state 和 action,返回新的 state。
(state, action) => newState
之所以稱作 reducer 是因為它將被傳遞給 Array.prototype.reduce(reducer, ?initialValue)
方法。保持 reducer 純凈非常重要。永遠不要在 reducer 里做以下操作:
- 修改傳入參數;
- 執行有副作用的操作,如 API 請求和路由跳轉;
- 調用非純函數,如
Date.now()
或Math.random()
。
在后續的學習終將會介紹如何執行有副作用的操作,現在只需謹記reducer一定要保持純凈。只要傳入參數相同,返回計算得到的下一個 state 就一定相同。沒有特殊情況、沒有副作用,沒有 API 請求、沒有變量修改,單純執行計算。
我們將寫一個reducer,讓它來處理之前定義過的action。我們可以首先指定state的初始狀態。
const initState = { /** 指定初始狀態 */
userName: 'World!'
}
export default function helloAppReducer(state=initState, action) {
switch(action.type) {
case 'CHANGE_USERNAME':
return {
userName: action.payload, // 改變狀態
};
default:
return state; // 返回舊狀態
}
}
警告:
-
不要修改
state
。如果涉及多個狀態時,可以采用對象展開運算符的支持,來返回一個新的狀態。 假設我們的實例中還存在其它狀態,但是我們只需要改變userName
的值,那么上述示例我們可以采用以下方式返回新的狀態:return { ...state, userName: action.payload }
在default情況下返回舊的
state
。 遇到未知的action時,一定要返回舊的state
。
Reducer拆分
這里我們以redux中文文檔 中的todo應用為例來說明,在應用的需求中,有添加todo項,設置todo列表的過濾條件等多個action,同理我們就需要寫多個reducer來描述狀態是怎么改變的,建議把todo列表的更新和設置過濾條件放在兩個reducer中去實現:
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [
...state,
{
text: action.text,
completed: false
}
]
case TOGGLE_TODO:
return state.map((todo, index) => {
if (index === action.index) {
return {
...todo,
completed: !todo.completed
}
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return {
...state,
visibilityFilter: action.filter
}
case ADD_TODO:
case TOGGLE_TODO:
return {
...state,
todos: todos(state.todos, action)
}
default:
return state
}
}
todos
依舊接收 state
,但它變成了一個數組!現在 todoApp
只把需要更新的一部分 state 傳給 todos
函數,todos
函數自己確定如何更新這部分數據。這就是所謂的 reducer 合成,它是開發 Redux 應用最基礎的模式。
現在我們可以開發一個函數來做為主 reducer,它調用多個子 reducer 分別處理 state 中的一部分數據,然后再把這些數據合成一個大的單一對象。主 reducer 并不需要設置初始化時完整的 state。初始時,如果傳入 undefined
, 子 reducer 將負責返回它們的默認值。這個過程就是reducer合并。
下面的這段代碼是reducer合并的兩種方式:
export default function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
每個 reducer 只負責管理全局 state 中它負責的一部分。每個 reducer 的 state 參數都不同,分別對應它管理的那部分 state 數據.
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
})
export default todoApp;
combineReducers()
所做的只是生成一個函數,這個函數來調用你的一系列 reducer,每個 reducer 篩選出 state 中的一部分數據并處理,然后這個生成的函數再將所有 reducer 的結果合并成一個大的對象。
Store
前面的部分,我們學會使用action來描述發生了什么,使用reducers來根據action更新state的用法。
Store則是把action和reducers聯系到一起的對象,它有以下職責:
- 維持應用的 state;
- 提供
getState()
方法獲取 state; - 提供
dispatch(action)
方法更新 state; - 通過
subscribe(listener)
注冊監聽器; - 通過
subscribe(listener)
返回的函數注銷監聽器。
再次說明Redux應用只有一個單一的store。 當需要拆分處理數據邏輯時,我們應該使用 reducer 組合 而不是創建多個 store。
根據已有的reducer來創建store是非常容易的。在我們的HelloApp應用中,我們將helloAppReducer
導入,并傳遞給createStore()
。
import { createStore } from 'redux'
import helloAppReducer from './reducers'
let store = createStore(helloAppReducer) // 創建store
createStore()
的第二個參數是可選的, 用于設置 state 初始狀態。
備注:
其實這種數據結構是有reducer確定的,就像helloAPP的例子中,
const reducer = combineReducers({
hello: hello,
city: cityReducer
})
而由redux-devtools
工具查看到的是下圖這樣的:
so,存儲在store中的數據結構是由reducer確定的。
數據流
嚴格的單向數據流 是Redux架構的核心設計。這就意味著應用中所有的數據都遵循相同的生命周期,這樣可以讓應用變得更加可預測且容易理解。同時也鼓勵做數據范式化,這樣可以避免使用多個且獨立的無法相互引用的重復數據。
Redux應用中數據的生命周期遵循以下4個步驟:
-
調用
store.dispatch(action)
。Action 就是一個描述“發生了什么”的普通對象。比如:
{ type: 'CHANGE_USERNAME', payload: "Welcome to Redux" };
我們可以在任何地方調用
store.dispatch(action)
包括組件中、XHR回調中、甚至是定時器中。 -
Redux store 調用傳入的 reducer 函數。
Store 會把兩個參數傳入 reducer: 當前的 state 樹和 action。
const initState = { /** 指定初始狀態 */ userName: 'World!' } export default function helloAppReducer(state=initState, action) { // 傳入兩個參數 switch(action.type) { case 'CHANGE_USERNAME': return { userName: action.payload, // 改變狀態 }; default: return state; // 返回當前狀態 } }
reducer 是純函數。它僅僅用于計算下一個 state。它應該是完全可預測的:多次傳入相同的輸入必須產生相同的輸出。它不應做有副作用的操作,如 API 調用或路由跳轉。這些應該在 dispatch action 前發生。
-
根 reducer 應該把多個子 reducer 輸出合并成一個單一的 state 樹。
根 reducer 的結構完全由我們自己決定。Redux 原生提供
combineReducers()
輔助函數,來把根 reducer 拆分成多個函數,用于分別處理 state 樹的一個分支。 -
Redux store 保存了根 reducer 返回的完整 state 樹。
這個新的樹就是應用的下一個state。所有訂閱
store.subscribe(listener)
的監聽器都將被調用;監聽器里可以調用store.getState()
獲取當前的state。
示例: Hello App
如果想查看示例的源碼,請查看這里。Hello App源碼
開始之前我們需要清楚實際上Redux和React之間并沒有關系。Redux支持React、Angular、Ember、jQuery甚至純JavaScript。即便如此,Redux 還是和 React 和 Deku 這類框架搭配起來用最好,因為這類框架允許你以 state 函數的形式來描述界面,Redux 通過 action 的形式來發起 state 變化。
下面我們將用React來開發一個Hello World的簡單應用。
安裝React Redux
Redux默認并不包含 React 綁定庫,需要單獨安裝。
npm install --save react-redux
容器組件和展示組件
Redux 的 React 綁定庫是基于 容器組件和展示組件相分離 的開發思想。而容器組件和展示組件大致有以下不同:
展示組件 | 容器組件 | |
---|---|---|
作用 | 描述如何展現內容、樣式 | 描述如何運行(數據獲取、狀態更新) |
是否能直接使用Redux | 否 | 是 |
數據來源 | props(屬性) | 監聽Redux state |
數據修改 | 從props中調用回調函數 | 向Redux派發actions |
調用方式 | 手動 | 通常由React Redux生成 |
大部分的組件都應該是展示型的,但一般需要少數的幾個容器組件把它們和Redux store連接起來。
技術上來說我們可以直接使用 store.subscribe()
來編寫容器組件。但不建議這么做,因為這樣寫就無法使用 React Redux 帶來的性能優化。同樣,不要手寫容器組件,我們直接使用 React Redux 的 connect()
方法來生成,后面會詳細介紹。
需求分析
我們的需求很簡單,我們只是想要展示hello + userName,默認為“Hello World!”,當我們在輸入框中輸入不同的值時,會顯示不同的“hello,___”問候語,由此可以分析出該應用只有一個狀態,那就是{ userName: '張三'}
展示組件
該應用只有一個展示組件HelloPanel:
-
HelloPanel
用于顯示輸入框及展示數據-
userName
: 要展示的數據 -
onChange(userName)
: 當輸入值發生變化時調用的回調函數
-
該組件之定義外觀并不涉及數據從哪里來,如果改變它,傳入什么就渲染什么,如果你把代碼從Redux遷移到別的架構,該組件可以不做任何改動直接使用。
容器組件
還需要一個容器組件來把展示組件連接到Redux。例如HelloPanel
組件需要一個狀態類似HelloApp的容器來監聽Redux store變化并處理如何過濾出要展示的數據。
HelloApp
根據當前顯示狀態來對展示組件進行渲染。
組件編碼
-
Action創建函數
action.js
export function changeUserName(userName) { return { type: 'CHANGE_USERNAME', payload: userName, }; }
-
Reducer
index.js
const initState = { /** 指定初始狀態 */ userName: 'World!' } export default function helloAppReducer(state=initState, action) { switch(action.type) { case 'CHANGE_USERNAME': return { userName: action.payload, // 改變狀態 }; default: return state; // 返回當前狀態 } }
-
展示組件
HelloPanel.js
import React from 'react'; export default function HelloPanel(props) { let input return ( <div> <p>Hello, {props.userName}</p> <input ref={node => { input = node }} onChange={()=>props.onChange(input.value)}/> </div> ); }
-
容器組件
使用
connect()
創建容器組件前,需要先定義mapStateToProps
這個函數來指定如何把當前 Redux store state 映射到展示組件的 props 中。例如:HelloApp
中需要計算const mapStateToProps = (state) => { return { userName: state.userName } // 返回期望注入到展示組件的props中的參數 };
除了讀取state,容器組件還能分發action。類似的方式,可以定義
mapDispatchToProps()
方法接收dispatch()
方法并返回期望注入到展示組件的 props 中的回調方法。const mapDispatchToProps = (dispatch) => ({ onChange: (userName) => { dispatch(changeUserName(userName)) // 返回期望注入到展示組件的 props 中的回調方法 } })
最后,使用
connect()
創建HelloApp
,并傳入這兩個函數。import { connect } from 'react-redux'; import HelloPanel from './HelloPanel'; const HelloApp = connect( // 產生一個新的組件 mapStateToProps, mapDispatchToProps, )(HelloPanel)
這就是 React Redux API 的基礎,但還漏了一些快捷技巧和強大的配置。建議仔細學習 React Redux文檔。如果你擔心
mapStateToProps
創建新對象太過頻繁,可以學習如何使用 reselect 來 計算衍生數據。
傳入Store
所有容器組件都可以訪問 Redux store,所以可以手動監聽它。一種方式是把它以 props 的形式傳入到所有容器組件中。但這太麻煩了,因此必須要用 store
把展示組件包裹一層,恰好在組件樹中渲染了一個容器組件。
建議的方式是使用指定的 React Redux 組件 <Provider>
來讓所有容器組件都可以訪問 store,而不必顯示地傳遞它。只需要在渲染根組件時使用即可。
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import HelloApp from './HelloApp'
import HelloReducer from './reducers'
let store = createStore(HelloReducer)
render(
<Provider store={store}>
<HelloApp />
</Provider>,
document.getElementById('root')
)
到這里,我們已經基本掌握了Redux的基礎及核心概念,有了這些,我們就可以開發簡單的應用,關于Redux的更多實例、高級應用、技巧、API文檔等可以查看redux中文文檔 。
子狀態樹與combineReducers(reducers)
簡介
隨著應用變得復雜,需要對 reducer 函數 進行拆分,拆分后的每一塊獨立負責管理 state 的一部分。
combineReducers
輔助函數的作用是,把一個由多個不同 reducer 函數作為 value 的 object,合并成一個最終的 reducer 函數,然后就可以對這個 reducer 調用 createStore
。
合并后的 reducer 可以調用各個子 reducer,并把它們的結果合并成一個 state 對象。state 對象的結構由傳入的多個 reducer 的 key 決定。
最終,state 對象的結構會是這樣的:
{
reducer1: ...
reducer2: ...
}
使用:
combineReducers({
hello, cityReducer
})
state 對象的結構:
// 實際例子
{
"hello":{"userName":"張三"},
"cityReducer":{"city":"北京"}
}
通過為傳入對象的 reducer 命名不同來控制 state key 的命名。
e.g.:
你可以調用 combineReducers({hello: hello,city: cityReducer})
將 state 結構變為{ hello, city }
通常的做法是命名 reducer,然后 state 再去分割那些信息,因此你可以使用 ES6 的簡寫方法:combineReducers({ hello, city })
。這與 combineReducers({ hello: hello,city: cityReducer })
一樣。
對于reducer的結構,我們規定只能是一級的,也就是
{
"hello":{"userName":"張三"},
"cityReducer":{"city":"北京"}
}
這種結構,不能再有子樹,這樣是為了方便進行管理。
參數
reducers
(Object)是一個對象,它的值(value) 對應不同的 reducer 函數,這些 reducer 函數后面會被合并成一個。下面會介紹傳入 reducer 函數需要滿足的規則。
之前的文檔曾建議使用 ES6 的
import * as reducers
語法來獲得 reducer 對象。這一點造成了很多疑問,因此現在建議在reducers/index.js
里使用combineReducers()
來對外輸出一個 reducer。下面有示例說明。
返回值
(Function):一個調用 reducers
對象里所有 reducer 的 reducer,并且構造一個與 reducers
對象結構相同的 state 對象。
注意
本函數設計的時候有點偏主觀,就是為了避免新手犯一些常見錯誤。也因些我們故意設定一些規則,但如果你自己手動編寫根 redcuer 時并不需要遵守這些規則。
每個傳入 combineReducers
的 reducer 都需滿足以下規則:
- 所有未匹配到的 action,必須把它接收到的第一個參數也就是那個
state
原封不動返回。 - 永遠不能返回
undefined
。當過早return
時非常容易犯這個錯誤,為了避免錯誤擴散,遇到這種情況時combineReducers
會拋異常。 - 如果傳入的
state
就是undefined
,一定要返回對應 reducer 的初始 state。根據上一條規則,初始 state 禁止使用undefined
。使用 ES6 的默認參數值語法來設置初始 state 很容易,但你也可以手動檢查第一個參數是否為undefined
。
實例:
const hello = (state = {userName: 'Hehe'}, action) => { // 設置了初始值
switch (action.type) {
case 'USER_CHANGE':
return {
userName: action.userName
}
// 所有未匹配到的 action,必須把它接收到的第一個參數也就是那個 state 原封不動返回。
default:
return state
}
}
export default hello
異步action
學習到這里,我們所接觸的下圖上的所有實現,都是針對同步事件的。如果只是這樣,那么我們肯定不能放心大膽的使用redux在我們的項目中,因為我們實際項目中,更多的都是異步事件。所以接下來,讓我們來介紹一個復雜的場景,我們來看看redux是如何應用在大型復雜充滿異步事件的場景中的。
我們仍然會遵守上圖,這是我們的核心,不能改變,下面我們來看一個實際的例子,工資列表頁面。
工資列表頁面
也就是一個普通的通過網絡請求,去請求列表數據的列表的展示。我們先來分析一下狀態,列表頁面的狀態。
狀態(state)
是一種數據結構,存儲在store中的數據
異步加載的頁面的狀態:“加載中;加載成功,展示列表;加載失敗” 這三種狀態。我們給這三種狀態來取一個名字,并設置0,1,2來順序表示不同的狀態。
loadingListStatus:0|1|2
我們主要做的是列表頁的展示,那么還有一個最重要的數據結構就是列表數據,我們來取一個名字:
salaryList:[]
接下來我們再來分析一下,action,也就是事件。
事件
列表展示過程中的數據,也就是:“開始加載;加載成功;加載失敗”這三個事件。其實整個過程和之前使用promise來實現的異步操作是一樣的。我們是監聽action,然后產生異步操作,執行dispatch方法,將數據結構保存到store中。
例子
我們來看一個獲取列表的請求:
function fetchSalayList(subreddit) {
return dispatch => {
dispatch(loadingAction(subreddit))// 開始加載
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => { // 加載成功
dispatch(loadingSucessAction(subreddit, json))
}, (error) => { // 加載失敗
dispatch(loadingErroeAction(subreddit))
}
}
}
上述這種方式,完全符合我們的核心圖表,并且實現了異步操作。
在異步操作這塊,我們建議使用 redux-saga 中間件來創建更加復雜的異步 action。其中涉及到es6中的Generators可以在文檔中查看。另外,還有 redux-saga的使用的一個例子可以看這里。
異步數據流
默認情況下,createStore()
所創建的 Redux store 沒有使用 middleware,所以只支持 同步數據流。
你可以使用 applyMiddleware()
來增強 createStore()
。雖然這不是必須的,但是它可以幫助你用簡便的方式來描述異步的 action。
像 redux-thunk 或 redux-promise 這樣支持異步的 middleware 都包裝了 store 的 dispatch()
方法,以此來讓你 dispatch 一些除了 action 以外的其他內容,例如:函數或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何內容,并繼續傳遞 actions 給下一個 middleware。比如,支持 Promise 的 middleware 能夠攔截 Promise,然后為每個 Promise 異步地 dispatch 一對 begin/end actions。
當 middleware 鏈中的最后一個 middleware 開始 dispatch action 時,這個 action 必須是一個普通對象。這是 同步式的 Redux 數據流 開始的地方(譯注:這里應該是指,你可以使用任意多異步的 middleware 去做你想做的事情,但是需要使用普通對象作為最后一個被 dispatch 的 action ,來將處理流程帶回同步方式)。
參考