Redux Reducer

2021-09-16 09:51 更新

Reducer

Action 只是描述了有事情發(fā)生了這一事實(shí),并沒有指明應(yīng)用如何更新 state。這是 reducer 要做的事情。

設(shè)計(jì) State 結(jié)構(gòu)

應(yīng)用所有的 state 都被保存在一個單一對象中(我們稱之為 state 樹)。建議在寫代碼前先想一下這個對象的結(jié)構(gòu)。如何才能以最簡的形式把應(yīng)用的 state 用對象描述出來?

以 todo 應(yīng)用為例,需要保存兩個不同的內(nèi)容:

  • 當(dāng)前選中的任務(wù)過濾條件;
  • 真實(shí)的任務(wù)列表。

通常,這個 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> 是比較好的方式(雖然你可以覺得冗余)。

Action 處理

現(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 里做這些操作:

  • 修改傳入?yún)?shù);
  • 執(zhí)行有副作用的操作,如 API 請求和路由跳轉(zhuǎn)。

高級篇里會介紹如何執(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;
  }
}

注意:

  1. 不要修改 state。 使用 Object.assign() 新建了一個副本。不能這樣使用 Object.assign(state, { visibilityFilter: action.filter }),因?yàn)樗鼤淖兊谝粋€參數(shù)的值。一定要把第一個參數(shù)設(shè)置為空對象。也可以使用 ES7 中還在試驗(yàn)階段的特性 { ...state, ...newState },參考 對象展開語法

  2. 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

還有兩個 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 前修改它。

拆分 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 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;
  }
}

上面代碼能否變得更通俗易懂?這里的 todosvisibilityFilter 的更新看起來是相互獨(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。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號