用功能分解和Reducer成分重构Reducer逻辑( Refactoring Reducer Logic Using Functional Decomposition and Reducer Composition )

用功能分解和Reducer成分重构Reducer逻辑( Refactoring Reducer Logic Using Functional Decomposition and Reducer Composition )

查看 sub-reducer 函数的不同类型以及它们如何组合在一起的示例会很有帮助。我们来看一个大型单个 reducer 函数如何被重构为几个较小函数的演示。

注意:为了说明重构的概念和过程,而不是完全简洁的代码,本示例有意用冗长的样式编写。

Initial Reducer

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

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

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

提取实用函数

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

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

这减少了重复,使事情变得更容易阅读。

Extracting Case Reducers

接下来,我们可以将每个特定的案例分解成它自己的函数:

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

现在清楚每种情况下发生了什么。我们也可以看到一些新兴的模式。

按域分隔数据处理

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

// 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) }; }

请注意,由于两个“切片状态”缩减器现在只将自己的整个状态的一部分作为参数获取,因此不再需要返回复杂的嵌套状态对象,因此现在变得更简单。

Reducing Boilerplate

我们差不多完成了。由于许多人不喜欢 switch 语句,因此使用一个函数可以创建一个查询表格来区分大小写函数。我们将使用 Reducing Boilerplate 中描述的createReducer函数:

// 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) }; }

Combining Reducers by Slice

作为最后一步,我们现在可以使用Redux的内置combineReducers实用程序来处理我们的顶级应用程序缩减器的 “slice-of-state” 逻辑。最终的结果如下:

// 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)以及 slice-of-state 处理程序(例如visibilityReducertodosReducer)。我们也可以看到这appReducer是一个 “ root reducer ” 的例子。

尽管本例中的最终结果明显比原始版本更长,但这主要是由于提取了效用函数,添加了注释以及为了清晰起见而有意为之,例如单独的返回语句。分别查看每个功能,责任的数量现在更小,并且希望更清楚。此外,在实际应用中,这些功能可能会再被分成单独的文件,例如reducerUtilities.jsvisibilityReducer.jstodosReducer.js,和rootReducer.js