# 使用函數分解(Functional Decomposition)和 Reducer 組合(Reducer Composition)重構 Reducer
看看不同類型的 `sub-reducer` 和如何把他們組合在一起的例子是很有用的。現在讓我們看看如何將一個大型的單個 reducer 重構為多個比較小的函數的組合。
> 注意: 為了說明重構的概念和過程而不是為了編寫簡潔的代碼,這個例子是特意以冗長的風格編寫的
#### 初遇 Reducer
讓我們看看初始 reducer 長什么樣:
```javascript
const initialState = {
visibilityFilter: 'SHOW_ALL',
todos: []
}
function appReducer(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.concat({
id: action.id,
text: action.text,
completed: false
})
})
}
case 'TOGGLE_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
completed: !todo.completed
})
})
})
}
case 'EDIT_TODO': {
return Object.assign({}, state, {
todos: state.todos.map(todo => {
if (todo.id !== action.id) {
return todo
}
return Object.assign({}, todo, {
text: action.text
})
})
})
}
default:
return state
}
}
```
這個函數非常短,但已經開始變得比較復雜。我們在處理兩個不同的區域(filtering 和 todo 列表),嵌套使得更新邏輯難以閱讀,并且會讓我們不清楚到底是什么跟什么。
#### 提取工具函數(Extracting Utility Functions)
第一步是寫一個返回更新了相應區域的新對象。這兒還有一個重復的邏輯是在更新數組中的特定項目,我們也可以將他提成一個函數。
```javascript
function updateObject(oldObject, newValues) {
// 用空對象作為第一個參數傳遞給 Object.assign,以確保是復制數據,而不是去改變原來的數據
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// 因為我們只想更新一個項目,所以保留所有的其他項目
return item
}
// 使用提供的回調來創建新的項目
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER': {
return updateObject(state, { visibilityFilter: action.filter })
}
case 'ADD_TODO': {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
case 'TOGGLE_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
case 'EDIT_TODO': {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
default:
return state
}
}
```
這樣就減少了重復,使得代碼的可讀性更高。
#### 提取 case reducer
接下來,把特殊邏輯封裝成對應的函數:
```javascript
// 省略了內容
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(state, action) {
return updateObject(state, { visibilityFilter: action.filter })
}
function addTodo(state, action) {
const newTodos = state.todos.concat({
id: action.id,
text: action.text,
completed: false
})
return updateObject(state, { todos: newTodos })
}
function toggleTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return updateObject(state, { todos: newTodos })
}
function editTodo(state, action) {
const newTodos = updateItemInArray(state.todos, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return updateObject(state, { todos: newTodos })
}
function appReducer(state = initialState, action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(state, action)
case 'ADD_TODO':
return addTodo(state, action)
case 'TOGGLE_TODO':
return toggleTodo(state, action)
case 'EDIT_TODO':
return editTodo(state, action)
default:
return state
}
}
```
現在很清楚每個 `case` 發生了什么。我們也可以看到一些模式的雛形。
#### 按域拆分數據(Separating Data Handling by Domain)
目前的 Reducer 仍然需要關心程序中所有不同的 case。下面嘗試把 filter 邏輯和 todo 邏輯分離:
```javascript
// 省略了內容
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function setVisibilityFilter(visibilityState, action) {
// 從技術上將,我們甚至不關心之前的狀態
return action.filter
}
function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return setVisibilityFilter(visibilityState, action)
default:
return visibilityState
}
}
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
function todosReducer(todosState = [], action) {
switch (action.type) {
case 'ADD_TODO':
return addTodo(todosState, action)
case 'TOGGLE_TODO':
return toggleTodo(todosState, action)
case 'EDIT_TODO':
return editTodo(todosState, action)
default:
return todosState
}
}
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
```
我們注意到,兩個 reducer 分別關心 state 中的不同的部分。都只需要把自身關心的數據作為參數,不再需要返回復雜的嵌套型 state 對象了,代碼變得更簡單。
#### 減少樣板代碼
馬上就大功告成了。因為很多人不喜歡使用 switch 這種語法結構,創建一個 action 到 case 查找表示非常通用的做法。可以使用 [縮減樣板代碼](./ReducingBoilerplate.md#generating-reducers) 中提到的 `createReducer` 函數減少樣板代碼。
```javascript
// 省略了內容
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// 省略了內容
function setVisibilityFilter(visibilityState, action) {}
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// 省略了內容
function addTodo(todosState, action) {}
function toggleTodo(todosState, action) {}
function editTodo(todosState, action) {}
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
function appReducer(state = initialState, action) {
return {
todos: todosReducer(state.todos, action),
visibilityFilter: visibilityReducer(state.visibilityFilter, action)
}
}
```
#### 通過切片組合 Reducer(Combining Reducers by Slice)
最后一步了,使用 Redux 中 `combineReducers` 這個工具函數去把管理每個 state 切片的邏輯組合起來,形成頂層的 reducer。最終變成這樣:
```javascript
// 可重用的工具函數
function updateObject(oldObject, newValues) {
// 將空對象作為第一個參數傳遞給 Object.assign,以確保只是復制數據,而不是去改變數據
return Object.assign({}, oldObject, newValues)
}
function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// 因為我們只想更新一個項目,所以保留所有的其他項目
return item
}
// 使用提供的回調來創建新的項目
const updatedItem = updateItemCallback(item)
return updatedItem
})
return updatedItems
}
function createReducer(initialState, handlers) {
return function reducer(state = initialState, action) {
if (handlers.hasOwnProperty(action.type)) {
return handlers[action.type](state, action)
} else {
return state
}
}
}
// 處理特殊 case 的 Handler ("case reducer")
function setVisibilityFilter(visibilityState, action) {
// 從技術上將,我們甚至不關心之前的狀態
return action.filter
}
// 處理整個 state 切片的 Handler ("slice reducer")
const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})
// Case reducer
function addTodo(todosState, action) {
const newTodos = todosState.concat({
id: action.id,
text: action.text,
completed: false
})
return newTodos
}
// Case reducer
function toggleTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { completed: !todo.completed })
})
return newTodos
}
// Case reducer
function editTodo(todosState, action) {
const newTodos = updateItemInArray(todosState, action.id, todo => {
return updateObject(todo, { text: action.text })
})
return newTodos
}
// Slice reducer
const todosReducer = createReducer([], {
ADD_TODO: addTodo,
TOGGLE_TODO: toggleTodo,
EDIT_TODO: editTodo
})
// 頂層 reducer
const appReducer = combineReducers({
visibilityFilter: visibilityReducer,
todos: todosReducer
})
```
現在我們有了分離集中 reducer 的例子:像 `updateObject` 和 `createReducer` 一樣的工具函數,像 `setVisibilityFilter` 和 `addTodo` 一樣的處理器(Handler),像 `visibilityReducer` 和 `todosReducer` 一樣的處理單個切片數據的 Handler。`appReducer` 可以被當作是頂層 reducer。
這個例子中最后的結果看上去比原始的版本更長,這主要是因為工具函數的提取,注釋的添加和一些為了清楚起見的故意冗長(比如單獨的 return 語句)。單獨的看每個功能,他們承擔的責任更小,意圖也更加清楚。在真正的應用中,這些函數將會分到單獨的文件中,比如:`reducerUtilities.js`,`visibilityReducer.js`,`todosReudcer.js` 和 `rootReducer.js`。
- 自述
- 介紹
- 動機
- 核心概念
- 三大原則
- 先前技術
- 學習資源
- 生態系統
- 示例
- 基礎
- Action
- Reducer
- Store
- 數據流
- 搭配 React
- 示例:Todo List
- 高級
- 異步 Action
- 異步數據流
- Middleware
- 搭配 React Router
- 示例:Reddit API
- 下一步
- 技巧
- 配置 Store
- 遷移到 Redux
- 使用對象展開運算符
- 減少樣板代碼
- 服務端渲染
- 編寫測試
- 計算衍生數據
- 實現撤銷重做
- 子應用隔離
- 組織 Reducer
- Reducer 基礎概念
- Reducer 基礎結構
- Reducer 邏輯拆分
- Reducer 重構示例
- combineReducers 用法
- combineReducers 進階
- State 范式化
- 管理范式化數據
- Reducer 邏輯復用
- 不可變更新模式
- 初始化 State
- 結合 Immutable.JS 使用 Redux
- 常見問題
- 綜合
- Reducer
- 組織 State
- 創建 Store
- Action
- 不可變數據
- 代碼結構
- 性能
- 設計哲學
- React Redux
- 其它
- 排錯
- 詞匯表
- API 文檔
- createStore
- Store
- combineReducers
- applyMiddleware
- bindActionCreators
- compose
- react-redux 文檔
- API
- 排錯