Skip to main content

使用函数分解和 Reducer 组合重构 Reducer 逻辑

¥Refactoring Reducer Logic Using Functional Decomposition and Reducer Composition

查看不同类型的子 reducer 函数的外观以及它们如何组合在一起的示例可能会有所帮助。让我们看一下如何将大型单个 reducer 函数重构为多个较小函数的组合的演示。

¥It may be helpful to see examples of what the different types of sub-reducer functions look like and how they fit together. Let's look at a demonstration of how a large single reducer function can be refactored into a composition of several smaller functions.

注意:这个示例故意以冗长的方式编写,以便说明重构的概念和过程,而不是完美简洁的代码。

¥Note: this example is deliberately written in a verbose style in order to illustrate the concepts and the process of refactoring, rather than perfectly concise code.

初始 reducer

¥Initial Reducer

假设我们的初始 reducer 如下所示:

¥Let's say that our initial reducer looks like this:

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
}
}

该函数相当短,但已经变得过于复杂。我们正在处理两个不同的关注字段(过滤与管理我们的待办事项列表),嵌套使更新逻辑更难以阅读,并且并不完全清楚到处发生了什么。

¥That function is fairly short, but already becoming overly complex. We're dealing with two different areas of concern (filtering vs managing our list of todos), the nesting is making the update logic harder to read, and it's not exactly clear what's going on everywhere.

提取实用函数

¥Extracting Utility Functions

一个好的第一步可能是分解一个实用函数来返回一个具有更新字段的新对象。还有一种重复模式,尝试更新数组中的特定项目,我们可以将其提取到函数中:

¥A good first step might be to break out a utility function to return a new object with updated fields. There's also a repeated pattern with trying to update a specific item in an array that we could extract to a function:

function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}

function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}

// Use the provided callback to create an updated 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
}
}

这减少了重复并使内容更容易阅读。

¥That reduced the duplication and made things a bit easier to read.

提取案例 reducer

¥Extracting Case Reducers

接下来,我们可以将每个具体情况拆分为自己的函数:

¥Next, we can split each specific case into its own function:

// Omitted
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
}
}

现在每种情况发生的情况都非常清楚。我们也可以开始看到一些模式的出现。

¥Now it's very clear what's happening in each case. We can also start to see some patterns emerging.

按域分离数据处理

¥Separating Data Handling by Domain

我们的应用 reducer 仍然了解我们应用的所有不同情况。让我们尝试将事情分开,以便将过滤器逻辑和待办事项逻辑分开:

¥Our app reducer is still aware of all the different cases for our application. Let's try splitting things up so that the filter logic and the todo logic are separated:

// Omitted
function updateObject(oldObject, newValues) {}
function updateItemInArray(array, itemId, updateItemCallback) {}

function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
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 现在仅获取整个状态中自己的部分作为参数,因此它们不再需要返回复杂的嵌套状态对象,因此现在变得更简单。

¥Notice that because the two "slice of state" reducers are now getting only their own part of the whole state as arguments, they no longer need to return complex nested state objects, and are now simpler as a result.

减少样板文件

¥Reducing Boilerplate

我们快完成了。由于许多人不喜欢 switch 语句,因此使用创建动作类型查找表到 case 函数的函数是很常见的。我们将使用 减少样板文件 中描述的 createReducer 函数:

¥We're almost done. Since many people don't like switch statements, it's very common to use a function that creates a lookup table of action types to case functions. We'll use the createReducer function described in Reducing Boilerplate:

// Omitted
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
}
}
}

// Omitted
function setVisibilityFilter(visibilityState, action) {}

const visibilityReducer = createReducer('SHOW_ALL', {
SET_VISIBILITY_FILTER: setVisibilityFilter
})

// Omitted
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 实用程序来处理顶层应用 reducer 的 "slice-of-state" 逻辑。这是最终结果:

¥As our last step, we can now use Redux's built-in combineReducers utility to handle the "slice-of-state" logic for our top-level app reducer. Here's the final result:

// Reusable utility functions

function updateObject(oldObject, newValues) {
// Encapsulate the idea of passing a new object as the first parameter
// to Object.assign to ensure we correctly copy data instead of mutating
return Object.assign({}, oldObject, newValues)
}

function updateItemInArray(array, itemId, updateItemCallback) {
const updatedItems = array.map(item => {
if (item.id !== itemId) {
// Since we only want to update one item, preserve all others as they are now
return item
}

// Use the provided callback to create an updated 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
}
}
}

// Handler for a specific case ("case reducer")
function setVisibilityFilter(visibilityState, action) {
// Technically, we don't even care about the previous state
return action.filter
}

// Handler for an entire slice of state ("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
})

// "Root reducer"
const appReducer = combineReducers({
visibilityFilter: visibilityReducer,
todos: todosReducer
})

我们现在有几种拆分 reducer 函数的示例:辅助实用程序(如 updateObjectcreateReducer)、特定情况的处理程序(如 setVisibilityFilteraddTodo)以及状态切片处理程序(如 visibilityReducertodosReducer)。我们还可以看到 appReducer 是 "根 reducer" 的一个示例。

¥We now have examples of several kinds of split-up reducer functions: helper utilities like updateObject and createReducer, handlers for specific cases like setVisibilityFilter and addTodo, and slice-of-state handlers like visibilityReducer and todosReducer. We also can see that appReducer is an example of a "root reducer".

尽管此示例中的最终结果明显比原始版本长,但这主要是由于实用函数的提取、注释的添加以及为了清晰起见而故意冗长的内容,例如单独的 return 语句。单独查看每个功能,现在的责任量更小,并且意图也更清晰。此外,在实际应用中,这些函数可能会被分割成单独的文件,例如 reducerUtilities.jsvisibilityReducer.jstodosReducer.jsrootReducer.js

¥Although the final result in this example is noticeably longer than the original version, this is primarily due to the extraction of the utility functions, the addition of comments, and some deliberate verbosity for the sake of clarity, such as separate return statements. Looking at each function individually, the amount of responsibility is now smaller, and the intent is hopefully clearer. Also, in a real application, these functions would probably then be split into separate files such as reducerUtilities.js, visibilityReducer.js, todosReducer.js, and rootReducer.js.