Reducer 最佳實踐月劈,Redux 開發(fā)最重要的部分

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ù)持久化等等。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末懂更,一起剝皮案震驚了整個濱河市眨业,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌膜蛔,老刑警劉巖坛猪,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件脖阵,死亡現(xiàn)場離奇詭異皂股,居然都是意外死亡,警方通過查閱死者的電腦和手機命黔,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門呜呐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人悍募,你說我怎么就攤上這事蘑辑。” “怎么了坠宴?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵洋魂,是天一觀的道長。 經(jīng)常有香客問我,道長副砍,這世上最難降的妖魔是什么衔肢? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮豁翎,結(jié)果婚禮上角骤,老公的妹妹穿的比我還像新娘。我一直安慰自己心剥,他們只是感情好邦尊,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著优烧,像睡著了一般蝉揍。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上畦娄,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天疑苫,我揣著相機與錄音,去河邊找鬼纷责。 笑死捍掺,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的再膳。 我是一名探鬼主播挺勿,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼喂柒!你這毒婦竟也來了不瓶?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤灾杰,失蹤者是張志新(化名)和其女友劉穎蚊丐,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體艳吠,經(jīng)...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡麦备,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了昭娩。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凛篙。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖栏渺,靈堂內(nèi)的尸體忽然破棺而出呛梆,到底是詐尸還是另有隱情,我是刑警寧澤磕诊,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布填物,位于F島的核電站纹腌,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏滞磺。R本人自食惡果不足惜壶笼,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望雁刷。 院中可真熱鬧覆劈,春花似錦、人聲如沸沛励。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽目派。三九已至坤候,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間企蹭,已是汗流浹背白筹。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谅摄,地道東北人徒河。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像送漠,于是被迫代替她去往敵國和親顽照。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355

推薦閱讀更多精彩內(nèi)容