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