reducer就是實現(xiàn)(state, action) => newState
的純函數(shù)呜袁,也就是真正處理state的地方敌买。值得注意的是,Redux并不希望你修改老的state阶界,而且通過直接返回新state的方式去修改虹钮。
在講如何設計reducer之前,先介紹幾個術(shù)語:
? reducer:實現(xiàn)(state, action) -> newState
的純函數(shù)膘融,可以根據(jù)場景分為以下好幾種
? root reducer:根reducer芙粱,作為createStore的第一個參數(shù)
? slice reducer:分片reducer,相對根reducer來說的托启。用來操作state的一部分數(shù)據(jù)宅倒。多個分片reducer可以合并成一個根reducer
? higher-order reducer:高階reducer攘宙,接受reducer作為參數(shù)的函數(shù)/返回reducer作為返回值的函數(shù)屯耸。
? case function:功能函數(shù),接受指定action后的更新邏輯蹭劈,可以是簡單的reducer函數(shù)疗绣,也可以接受其他參數(shù)。
reducer的最佳實踐主要分為以下幾個部分
? 抽離工具函數(shù)铺韧,以便復用多矮。
? 抽離功能函數(shù)(case function),精簡reducer聲明部分的代碼哈打。
? 根據(jù)數(shù)據(jù)類別拆分塔逃,維護多個獨立的slice reducer。
? 合并slice reducer料仗。
? 通過crossReducer在多個slice reducer中共享數(shù)據(jù)湾盗。
? 減少reducer的模板代碼。
接下來立轧,我們詳細的介紹每個部分
如何抽離工具函數(shù)格粪?
抽離工具函數(shù),幾乎在任何一個項目中都需要氛改。要抽離的函數(shù)需要滿足以下條件:
? 純凈帐萎,和業(yè)務邏輯不耦合
? 功能單一,一個函數(shù)只實現(xiàn)一個功能
由于reducer都是對state的增刪改查胜卤,所以會有較多的重復的基礎邏輯疆导,針對reducer來抽離工具函數(shù),簡直恰到好處葛躏。
// 比如對象更新是鬼,淺拷貝
export const updateObject = (oldObj, newObj) => {
return assign({}, oldObj, newObj);
}
// 比如對象更新肤舞,深拷貝
export const deepUpdateObject = (oldObj, newObj) => {
return deepAssign({}, oldObj, newObj);
}
工具函數(shù)抽離出來,建議放到單獨的文件中保存均蜜。
如何抽離 case function 功能函數(shù)李剖?
不要被什么case function嚇到,直接給你看看代碼你就清楚了囤耳,也是體力活篙顺,目的是為了讓reducer的分支判斷更清晰。
// 抽離前充择,所有代碼都揉到slice reducer中德玫,不夠清晰
function appreducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
...
...
return newState;
case 'TOGGLE_TODO':
...
...
return newState;
default:
return state;
}
}
// 抽離后,將所有的state處理邏輯放到單獨的函數(shù)中椎麦,reducer的邏輯格外清楚
function addTodo(state, action) {
...
...
return newState;
}
function toggleTodo(state, action) {
...
...
return newState;
}
function appreducer(state = initialState, action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(state, action);
case 'TOGGLE_TODO':
return toggleTodo(state, action);
default:
return state;
}
}
case function就是指定action的處理函數(shù)宰僧,是最小粒度的reducer。
抽離case function观挎,可以讓slice reducer的代碼保持結(jié)構(gòu)上的精簡琴儿。
如何設計slice reducer?
上一篇 關(guān)于state的博客 已經(jīng)提過嘁捷,我們需要對state進行拆分處理造成,然后用對應的slice reducer去處理對應的數(shù)據(jù),比如article相關(guān)的數(shù)據(jù)用articlesReducer去處理雄嚣,paper相關(guān)的數(shù)據(jù)用papersReducer去處理晒屎。
這樣可以保證數(shù)據(jù)之間解耦,并且讓每個slice reducer保持代碼清晰并且相對獨立缓升。
比如好奇心日報有articles鼓鲁、papers兩個類別的數(shù)據(jù),我們拆分state并扁平化改造
{
// 扁平化
entities: {
articles: {},
papers: {}
},
// 按類別拆分數(shù)據(jù)
articles: {
list: []
},
papers: {
list: []
}
}
為了對state.articles和state.papers分別進行管理港谊,我們設計兩個slice reducer骇吭,分別是articlesReducer和papersReducer
// ------------------------------------
// Action Handlers
// ------------------------------------
const ACTION_HANDLERS = {
[UPDATE_ARTICLES_LIST]: updateArticelsList(articles, action)
}
// ------------------------------------
// reducer
// ------------------------------------
// !!!值得注意的是,對于articlesReducer來說封锉,它并不知道state的存在绵跷,它只知道state.articles!!!
// 所以articlesReducer完成的工作是(articles, action) => newArticles
export function articlesReducer(articles = {
list: []
}, action) {
const handler = ACTION_HANDLERS[action.type]
return handler ? handler(articles, action) : articles
}
// papersReducer類似,就不貼代碼了成福。
由于我們的state進行了扁平化改造碾局,所以我們需要在case function中進行normalizr化。
根據(jù)state的拆分奴艾,設計出對應的slice reducer净当,讓他們對自己的數(shù)據(jù)分別管理,這樣后代碼更便于維護,但也引出了兩個問題像啼。
? 拆分多個slice reducer俘闯,但createStore只能接受一個reducer作為參數(shù),所以我們怎么合并這些slice reducer呢忽冻?
? 每個slice reducer只負責管理自身的數(shù)據(jù)真朗,對state并不知情。那么articlesReducer怎么去改變state.entities的數(shù)據(jù)呢僧诚?
這兩個問題遮婶,分別引出了兩部分內(nèi)容,分別是:slice reducer合并湖笨、slice reducer數(shù)據(jù)共享旗扑。
如何合并多個slice reducer?
redux提供了combineReducer方法慈省,可以用來合并多個slice reducer臀防,返回root reducer傳遞給createStore使用。直接上代碼边败,非常簡單袱衷。
combineReducers({
entities: entitiesreducer,
// 對于articlesReducer來說,他接受(state, action) => newState,
// 其中的state放闺,是articles祟昭,也就是state.articles
// 它并不能獲取到state的數(shù)據(jù)缕坎,更不能獲取到state.papers的數(shù)據(jù)
articles: articlesReducer,
papers: papersReducer
})
傳遞給combineReducer的是key-value 鍵值對
怖侦,其中鍵表示傳遞到對應reducer的數(shù)據(jù),也就是說:slice reducer中的state并不是全局state谜叹,而是state.articles/state.papers
等數(shù)據(jù)匾寝。
如果解決多個slice reducer間共享數(shù)據(jù)的問題?
slice reducer本質(zhì)上是為了實現(xiàn)專門數(shù)據(jù)專門管理荷腊,讓數(shù)據(jù)管理更清晰艳悔。那么slice reducer間如何共享數(shù)據(jù)呢?
舉個例子女仰,我們異步獲取article的時候猜年,會附帶將comments也帶過來,那么我們在articlesReducer中怎么去維護這份comments數(shù)據(jù)疾忍?
// 不好的方法
// 我們通過兩次dispatch來分別更新comments和article
// 缺點是:slice reducer之間嚴重耦合乔外,代碼不容易維護
dispatch(updateComments(comments));
dispatch(updateArticle(article)));
那么有什么更好的辦法呢?我們能不能在articlesReducer處理之后一罩,將action透傳給commentsReducers呢杨幼?看看如下代碼
// 定義一個crossReducer
function crossReducer(state, action) {
switch (action.type) {
// 處理指定的action
case UPDATE_COMMENTS:
return Object.assign({}, state, {
// 這兒是關(guān)鍵,相當于透傳到commentsReducer,然后讓commentsReducer去處理對應的邏輯差购。
// 這樣的話
// crossReducer不關(guān)心commentsReducer的邏輯
// articlesReducer也不用去關(guān)心commentsReducer的邏輯
comments: commentsReducer(state.comments, action)
});
default:
return state;
}
}
let combinedReducer = combineReducers({
entities: entitiesreducer,
articles: articlesReducer,
papers: papersReducer
});
// 在其他reducer處理完成后四瘫,在進行crossReducer的操作
function rootReducer(state, action) {
let tempstate = combinedReducer(state, action),
finalstate = crossReducer(tempstate, action);
return finalstate;
}
當然,我們可以使用reduce-reducers這個插件來簡化上面的rootReducer欲逃。
import reduceReducers from 'reduce-reducers';
export const rootReducer = reduceReducers(
combineReducers({
entities: entitiesreducer,
articles: articlesReducer,
comments: commentsReducer
}),
crossReducer
);
原理很簡單找蜜,先執(zhí)行某些slice reducer,執(zhí)行完成后稳析,再去執(zhí)行crossReducer锹杈,而crossReducer本身不做任何的工作,只負責調(diào)用關(guān)聯(lián)reducer迈着,并且把數(shù)據(jù)傳到關(guān)聯(lián)reducer中竭望。
如何減少reducer的樣板代碼?
每次寫action/action creator/reducer
裕菠,都會寫很多相似度很高的代碼咬清,我們是否可以通過一定封裝,來減少這些樣板代碼呢奴潘?
比如我們定義一個createReducer的函數(shù)旧烧,用來創(chuàng)建slice reducer。如下所示:
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
const todosreducer = createReducer([], {
'ADD_TODO': addTodo,
'TOGGLE_TODO': toggleTodo,
'EDIT_TODO': editTodo
});
也可以使用現(xiàn)成的比較好的方案画髓,比如:redux-actions掘剪。給個簡單的示例,更多的可以查看官方文檔奈虾。
// 定義action及action creator
const {
increment,
descrement
} = createActions({
INCREMENT: (val) => val,
DECREMENT: (val) => val
});
// 定義reducer
const reducer = handleActions({
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
}, { counter: 0 });
減少樣板代碼之后夺谁,代碼一下就變得清晰多了。
總結(jié)說點啥肉微?
reducer的設計相對于state和action來說要復雜很多匾鸥,他涉及拆分、合并碉纳、數(shù)據(jù)共享的問題勿负。
本文介紹了怎樣最佳實踐的去設計reducer,按照上面的步驟下來劳曹,可以讓你的reducer保持結(jié)構(gòu)簡單奴愉。
? 抽離工具函數(shù),這個不用多說铁孵。
? 抽離case function锭硼,讓slice reducer看起來更簡潔。其中case function是最小粒度的reducer库菲,是action的處理函數(shù)账忘。
? 拆分slice reducer,這個是和state拆分匹配的,拆分slice reducer是為了實現(xiàn)專門數(shù)據(jù)專門管理鳖擒,并且讓slice reducer更加便于維護溉浙。
? 合并slice reducer,createStore只能接受一個reducer作為參數(shù)蒋荚,所以我們用combineReducer將拆分后的slice reducer合并起來戳稽。先拆分再合并其實更多是為了工程上的便利。
? 使用crossReducer類似的功能期升,可以實現(xiàn)slice reducer間數(shù)據(jù)共享惊奇。
? 減少reducer的樣板代碼,這個不多說播赁,使用redux-actions就挺好颂郎,但不建議新人這樣做。
實際開發(fā)中容为,我個人更喜歡將action和reducer寫在一個文件中乓序,并且將redux相關(guān)的代碼全部放到統(tǒng)一的目錄中。
結(jié)合上一篇博客講的 state設計坎背,Redux基本的架構(gòu)雛形就出來了替劈,當然可以繼續(xù)深入,比如結(jié)合按需加載得滤、路由陨献、數(shù)據(jù)持久化等等。