Action 只是描述了有事情發(fā)生了這一事實(shí),并沒有指明應(yīng)用如何更新 state。這是 reducer 要做的事情。
應(yīng)用所有的 state 都被保存在一個單一對象中(我們稱之為 state 樹)。建議在寫代碼前先想一下這個對象的結(jié)構(gòu)。如何才能以最簡的形式把應(yīng)用的 state 用對象描述出來?
以 todo 應(yīng)用為例,需要保存兩個不同的內(nèi)容:
通常,這個 state 樹還需要存放其它一些數(shù)據(jù),還有界面 state。這樣做沒問題,但盡量把這些數(shù)據(jù)與界面 state 分開。
{
visibilityFilter: 'SHOW_ALL',
todos: [{
text: 'Consider using Redux',
completed: true,
}, {
text: 'Keep all state in a single tree',
completed: false
}]
}
處理 Reducer 關(guān)系時注意
開發(fā)復(fù)雜的應(yīng)用時,不可避免會有一些數(shù)據(jù)相互引用。建議你盡可能地把 state 范式化,不存在嵌套。把所有數(shù)據(jù)放到一個對象里,每個數(shù)據(jù)以 ID 為主鍵,不同數(shù)據(jù)相互引用時通過 ID 來查找。把應(yīng)用的 state 想像成數(shù)據(jù)庫。這種方法在 normalizr 文檔里有詳細(xì)闡述。例如,實(shí)際開發(fā)中,在 state 里同時存放
todosById: { id -> todo }
和todos: array<id>
是比較好的方式(雖然你可以覺得冗余)。
現(xiàn)在我們已經(jīng)確定了 state 對象的結(jié)構(gòu),就可以開始開發(fā) reducer。reducer 就是一個函數(shù),接收舊的 state 和 action,返回新的 state。
(previousState, action) => newState
之所以稱作 reducer 是因?yàn)楹?Array.prototype.reduce(reducer, ?initialValue)
格式很像。保持 reducer 純凈非常重要。永遠(yuǎn)不要在 reducer 里做這些操作:
在高級篇里會介紹如何執(zhí)行有副作用的操作。現(xiàn)在只需要謹(jǐn)記 reducer 一定要保持純凈。只要傳入?yún)?shù)一樣,返回必須一樣。沒有特殊情況、沒有副作用,沒有 API 請求、沒有修改參數(shù),單純執(zhí)行計(jì)算。
明白了這些之后,就可以開始編寫 reducer,并讓它來處理之前定義過的 actions。
我們在開始時定義默認(rèn)的 state。Redux 首次執(zhí)行時,state 為 undefined
,這時候會返回默認(rèn) state。
import { VisibilityFilters } from './actions';
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
};
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState;
}
// 這里暫不處理任何 action,
// 僅返回傳入的 state。
return state;
}
這里一個技巧是使用 ES6 參數(shù)默認(rèn)值語法 來精簡代碼。
function todoApp(state = initialState, action) {
// 這里暫不處理任何 action,
// 僅返回傳入的 state。
return state;
}
現(xiàn)在可以處理 SET_VISIBILITY_FILTER
。需要做的只是改變 state 中的 visibilityFilter
。
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
default:
return state;
}
}
注意:
不要修改 state
。 使用 Object.assign()
新建了一個副本。不能這樣使用 Object.assign(state, { visibilityFilter: action.filter })
,因?yàn)樗鼤淖兊谝粋€參數(shù)的值。一定要把第一個參數(shù)設(shè)置為空對象。也可以使用 ES7 中還在試驗(yàn)階段的特性 { ...state, ...newState }
,參考 對象展開語法。
在 default
情況下返回舊的 state
。遇到未知的 action 時,一定要返回舊的 state
。
Object.assign
使用提醒
Object.assign()
是 ES6 特性,但多數(shù)瀏覽器并不支持。你要么使用 polyfill,Babel 插件,或者使用其它庫如_.assign()
提供的幫助方法。
switch
和樣板代碼提醒
state
語句并不是嚴(yán)格意義上的樣板代碼。Flux 中真實(shí)的樣板代碼是概念性的:更新必須要發(fā)送、Store 必須要注冊到 Dispatcher、Store 必須是對象(開發(fā)同構(gòu)應(yīng)用時變得非常復(fù)雜)。為了解決這些問題,Redux 放棄了 event emitters(事件發(fā)送器),轉(zhuǎn)而使用純 reducer。很不幸到現(xiàn)在為步,還有很多人存在一個誤區(qū):根據(jù)文檔中是否使用
switch
來決定是否使用它。如果你不喜歡switch
,完全可以自定義一個createReducer
函數(shù)來接收一個事件處理函數(shù)列表,參照"減少樣板代碼"。
還有兩個 action 需要處理。讓我們先處理 ADD_TODO
。
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
}]
});
default:
return state;
}
}
如上,不直接修改 state
中的字段,而是返回新對象。新的 todos
對象就相當(dāng)于舊的 todos
在末尾加上新建的 todo。而這個新的 todo 又是在 action 中創(chuàng)建的。
最后,COMPLETE_TODO
的實(shí)現(xiàn)也很好理解:
case COMPLETE_TODO:
return Object.assign({}, state, {
todos: [
...state.todos.slice(0, action.index),
Object.assign({}, state.todos[action.index], {
completed: true
}),
...state.todos.slice(action.index + 1)
]
});
因?yàn)槲覀儾荒苤苯有薷膮s要更新數(shù)組中指定的一項(xiàng)數(shù)據(jù),這里需要先把前面和后面都切開。如果經(jīng)常需要這類的操作,可以選擇使用幫助類 React.addons.update,updeep,或者使用原生支持深度更新的庫 Immutable。最后,時刻謹(jǐn)記永遠(yuǎn)不要在克隆 state
前修改它。
目前的代碼看起來有些冗余:
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 COMPLETE_TODO:
return Object.assign({}, state, {
todos: [
...state.todos.slice(0, action.index),
Object.assign({}, state.todos[action.index], {
completed: true
}),
...state.todos.slice(action.index + 1)
]
});
default:
return state;
}
}
上面代碼能否變得更通俗易懂?這里的 todos
和 visibilityFilter
的更新看起來是相互獨(dú)立的。有時 state 中的字段是相互依賴的,需要認(rèn)真考慮,但在這個案例中我們可以把 todos
更新的業(yè)務(wù)邏輯拆分到一個單獨(dú)的函數(shù)里:
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
});
case ADD_TODO:
case COMPLETE_TODO:
return Object.assign({}, state, {
todos: todos(state.todos, action)
});
default:
return state;
}
}
注意 todos
依舊接收 state
,但它變成了一個數(shù)組!現(xiàn)在 todoApp
只把需要更新的一部分 state 傳給 todos
函數(shù),todos
函數(shù)自己確定如何更新這部分?jǐn)?shù)據(jù)。這就是所謂的 reducer 合成,它是開發(fā) Redux 應(yīng)用最基礎(chǔ)的模式。
下面深入探討一下如何做 reducer 合成。能否抽出一個 reducer 來專門管理 visibilityFilter
?當(dāng)然可以:
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}
現(xiàn)在我們可以開發(fā)一個函數(shù)來做為主 reducer,它調(diào)用多個子 reducer 分別處理 state 中的一部分?jǐn)?shù)據(jù),然后再把這些數(shù)據(jù)合成一個大的單一對象。主 reducer 并不需要設(shè)置初始化時完整的 state。初始時,如果給子 reducer 傳入 undefined
只要返回它們的默認(rèn)值即可。
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
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)
};
}
注意每個 reducer 只負(fù)責(zé)管理全局 state 中它負(fù)責(zé)的一部分。每個 reducer 的 state
參數(shù)都不同,分別對應(yīng)它管理的那部分 state 數(shù)據(jù)。
現(xiàn)在看過起來好多了!隨著應(yīng)用的膨脹,我們已經(jīng)學(xué)會把 reducer 拆分成獨(dú)立文件來分別處理不同的數(shù)據(jù)域了。
最后,Redux 提供了 combineReducers()
工具類來做上面 todoApp
做的事情,這樣就能消滅一些樣板代碼了。有了它,可以這樣重構(gòu) todoApp
:
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,
todos
});
export default todoApp;
注意上面的寫法和下面完全等價:
export default function todoApp(state, action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter, action),
todos: todos(state.todos, action)
};
}
你也可以給它們設(shè)置不同的 key,或者調(diào)用不同的函數(shù)。下面兩種合成 reducer 方法完全等價:
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
});
function reducer(state, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
};
}
combineReducers()
所做的只是生成一個函數(shù),這個函數(shù)來調(diào)用你的一系列 reducer,每個 reducer 根據(jù)它們的 key 來篩選出 state 中的一部分?jǐn)?shù)據(jù)并處理,然后這個生成的函數(shù)所所有 reducer 的結(jié)果合并成一個大的對象。沒有任何魔法。
ES6 用戶使用注意
combineReducers
接收一個對象,可以把所有頂級的 reducer 放到一個獨(dú)立的文件中,通過export
暴露出每個 reducer 函數(shù),然后使用import * as reducers
得到一個以它們名字作為 key 的 object:
import { combineReducers } from 'redux';
import * as reducers from './reducers';
const todoApp = combineReducers(reducers);
由于
import *
還是比較新的語法,為了避免困惑,我們不會在文檔使用它。但在一些社區(qū)示例中你可能會遇到它們。
reducers.js
import { combineReducers } from 'redux';
import { ADD_TODO, COMPLETE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions';
const { SHOW_ALL } = VisibilityFilters;
function visibilityFilter(state = SHOW_ALL, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return action.filter;
default:
return state;
}
}
function todos(state = [], action) {
switch (action.type) {
case ADD_TODO:
return [...state, {
text: action.text,
completed: false
}];
case COMPLETE_TODO:
return [
...state.slice(0, action.index),
Object.assign({}, state[action.index], {
completed: true
}),
...state.slice(action.index + 1)
];
default:
return state;
}
}
const todoApp = combineReducers({
visibilityFilter,
todos
});
export default todoApp;
接下來會學(xué)習(xí) 創(chuàng)建 Redux store。store 能維持應(yīng)用的 state,并在當(dāng)你發(fā)起 action 的時候調(diào)用 reducer。
更多建議: