書籍完整目錄
3.1 開始使用 redux
前面我們介紹了 flux 架構以及其開源實現 redux,在這一節中,我們將完整的介紹 redux:
-
redux 介紹
-
redux 是什麼
-
redux 概念
-
redux 三原則
-
-
redux Stores
-
redux Action
-
redux Reducers
-
redux 數據流動
3.1.1 redux 介紹
redux 是什麼
Redux is a predictable state container for JavaScript apps
譯:Redux 是為 Javascript 應用而生的可預估的狀態容器
定義有些抽象,簡單來講 redux 可以理解為基於 flux 和其他一些思想(Elm,函數式編程)發展出來的前端應用架構庫,作為一個前端數據狀態容器體現,並可以在 React 和其他任何前端框架中使用。
redux 概念
-
store: 同 flux,應用數據的存儲中心
-
action: 同 flux ,應用數據的改變的描述
-
reducer: 決定應用數據新狀態的函數,接收應用之前的狀態和一個 action 返回數據的新狀態。
-
middleware: redux 提供中間件的方式,完成一些 flux 流程的自定義控制,同時形成其插件體系
redux 三原則
單一的 store
區別於 flux 模式中可以有多個 state,整個應用的數據以樹狀結構存放在一個 state 對象中。
console.log(store.getState())
/* Prints
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
state 只讀
state 包含的應用數據不能隨意修改,修改數據的唯一方式是 dispatch action,action 描述要修改的信息(這和 flux 架構上是一致的,不過在設計上更加嚴格,見後面的 reducer 設計)。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
數據的改變由純函數生成
在 redux 中,應用數據的改變路徑為:
-
store.dispatch(action)
-
newState = reducer(previousState, action)
-
reducer 為純函數
純函數是函數是編程的思想,只要參數相同,函數返回的結果永遠是一致的,並且不會對外部有任何影響(不會改變參數對象的數據)。也就是説 reducer 每次必須返回一個新狀態,新狀態由舊狀態和 action 數據決定。
3.1.2 安裝 redux
安裝 redux 核心和 react-redux 集成進 react
$ npm install --save redux
$ npm install --save react-redux
3.1.3 redux store
在 redux 中 store 作為一個單例,存放了應用的所有數據,對外提供瞭如下的 API:
-
獲取數據
store.getState() -
通過觸發 action 來更新數據
store.dispatch(action) -
pubsub 模式提供消息訂閲和取消
store.subscribe(listener)
創建並初始化 store
redux 提供 createStore 方法來創建一個 store
/**
* [createStore description]
* @param {[type]} reducer [reducer 函數]
* @param {[type]} initialState [應用初始化狀態]
* @return {[type]} [description]
*/
function createStore(reducer, initialState) {
// ....
}
創建一個 store :
var redux = require('redux');
var todoAppReducer = require('./reducers');
var initalState = {
todos: []
};
var store = redux.createStore(todoAppReducer, initialState);
觸發 action
redux 修改應用數據的唯一方式為 store.dispatch(action)
eg:
store.dispatch({
type: 'ADD_TODO',
title: 'new todo'
})
消息訂閲和取消
為了讓用户監聽應用數據改變,redux 在 store 集成了 pubsub 模式
訂閲
// 每次應用數據改變,輸出最新的應用數據
store.subscribe(function(){
console.log(store.getState())
})
// 如果新增了 todo 會觸發上面的訂閲函數
store.dispatch({
type: 'ADD_TODO',
title: 'new todo'
})
取消
subscribe 返回的函數即為取消函數
var unsubscribe = store.subscribe(function(){
console.log(store.getState())
})
// ....
unsubscribe();
設計應用數據結構
所有數據都存放在一個 store 中,以 todoApp 為例子,state 的數據結構可能為
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
當應用變大的時候,數據結構可能沒有這麼簡單,這時候需要找到一個較好的結構來設計應用數據,下面是兩個 redux 在設計 state 上的 tip:
-
業務數據和 UI 狀態數據分離,儘量避免 UI 狀態數據放在 store 中,即便放在 store 中也好和業務數據分離。
-
避免嵌套,在一個複雜的場景,數據對象可能很深,出現多層,那在設計的時候可以通過 id 的方式來引用,可以參考 normalizr 的簡化方式
3.1.4 redux action
我們已經知道 action 就是數據修改的描述信息,不過在實際使用過程中需要理解下面的這些規範:
-
action 描述數據結構
-
action 類型常量
-
action creator
action 描述數據結構
redux 對 action 對象的數據結構做了簡單規範,每個對象必須包含一個字段 type,應用通過 type 來識別 action 類型,其他字段不做任何限制。
eg:
{
type: "ADD_TODO",
text: 'Build my first Redux app'
}
{
type: "REMVOE_TODO",
index: 1
}
{
type: "TOGGLE_TODO",
id: 'a1s2d1'
}
action 類型常量
為了項目的規範,通常把 action type 定義為名稱常量,放在一個單獨的文件中管理,這在大型項目中是一個很好的習慣。
eg:
var ADD_TODO = "ADD_TODO"
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
action creator
在 flux 模式小節已經介紹過,為了規範化 action 通過 action creator 來創建
function addTodo(text) {
return {
type: ADD_TODO,
text: text
}
}
原來的 dispatch 的使用改為了
// 舊的方式
store.dispatch({
type: ADD_TODO,
text: text
})
// 新的方式
store.dispatch(addTodo(text))
3.1.5 redux reducer
reducer 應該是最為陌生的概念,理解 reducer 是理解 redux 的關鍵,牢記 reducer 決定應用數據的改變
reducer 基礎
/**
* [reducer description]
* @param {[type]} previewsState [之前狀態]
* @param {[type]} action [redux action]
* @return {[type]} newState [新狀態]
*/
function reducer(previewsState, action) {
var newState;
switch(action.type) {
case ..
case ..
default:
newState = previewsState
}
return newState;
}
首先 reducer 是一個純函數,接收到之前的應用狀態和 action 並返回一個新狀態,為了每次返回一個新狀態,可以通過 Object.assign() 方法返回一個新的對象,也可以使用 Immutable.js (後面的章節會講解 Immutable.js)。
function todoApp(state, action) {
state = initialState
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
redux 會在初始的時候調用一次 reducer (這時參數 previewsState 為 undefined), 可以借用這個機會返回應用初始狀態數據。
eg:
// reducers/todoApp.js
var initialState = {todos: ...};
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState
}
// 返回默認值
return state
}
module.exports = todoApp;
總結需要注意的點:
-
純函數特性,不能修改 previewsState,不能調用異步 API,無論什麼時候,傳入相同的參數都能立即返回相同的結果(不能調用 Math.random, Data.now 這些函數,因為會導致不同的結果)
-
默認返回 previewsState (在 action 不會得到處理的情況)
-
處理 state 為 undefined 的情況
reducer 組合
一個 todo 應用可能有很多操作,在真實場景上面的 todoApp reducer 可能膨脹為如下的結構:
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
case ADD_TODO:
return Object.assign({}, state, {
todos: [
...state.todos,
{
text: action.text,
completed: false
}
]
})
case TOGGLE_TODO:
return Object.assign({}, state, {
todos: state.todos.map((todo, index) => {
if(index === action.index) {
return Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
})
default:
return state
}
}
這時候可以對 todoApp reducer 做拆分,將它拆分為多個不同的 reducer,todoApp reducer 的作用只是組合其他 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 Object.assign({}, todo, {
completed: !todo.completed
})
}
return todo
})
default:
return state
}
}
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter
default:
return state
}
}
function todoApp(state = {}, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
}
}
module.exports = todoApp
todoApp 不再負責整個應用狀態的修改,而是將責任分配給了其他的 reducer, 每個子 reducer 相互獨立,負責各自的責任,這個時候應用數據的改變不是靠一個 reducer 改變的,而是由多個 reducer 組合起來,這是 redux 應用的基礎,叫做 reducer 組合。
可以發現 reducer 的組合和狀態數據的結構是相同的,都可以理解為樹狀結構。 state 根對象有一個根 reducer (createState 傳入的 reducer)這裏是 todoApp , state 對象下面的第一級數據對應不用的 reducer,visibilityFilter 和 todos , 理解起來這也是一個典型的 分而治之策略。
對於 reducer 的組合,redux 提供了 combineReducers() 方法來簡化,上面的 todoApp 可以簡化為:
var todoApp = redux.combineReducers({
visibilityFilter: visibilityFilter,
todos: todos
})
這樣的寫法和上面的寫法作用完全相同
3.1.5 redux 數據流動
redux 繼承 flux 最核心的地方是 數據的單向流動,
上面已經詳細介紹了 redux 的各個概念,下面將這些概念結合起來,看看數據怎麼在 redux 中流動的。
第一步:調用 store.dispatch(action)
可以在任何地方觸發 dispatch,例如 UI 交互,API 調用
第二步: Redux store 調用 rootReducer
redux 收到 action 過後,調用根 reducer 並返回最新的狀態數據。(根 reducer 內部組合其他 reducer 返回部分的最新狀態)
第三步:接收新狀態並 publish 給訂閲者
當 rootReducer 返回最新的狀態後,通知訂閲函數 store.subscribe(listener) 。在 React 中,可以訂閲狀態更新,在訂閲函數中獲取最新的狀態過後,修改根組件的數據:
component.setState(newState)