使用函数分解(Functional Decomposition)和 Reducer 组合(Reducer Composition)重构 Reducer

看看不同类型的 sub-reducer 和如何把他们组合在一起的例子是很有用的。现在让我们看看如何将一个大型的单个 reducer 重构为多个比较小的函数的组合。

注意: 为了说明重构的概念和过程而不是为了编写简洁的代码,这个例子是特意以冗长的风格编写的

初遇 Reducer

让我们看看初始 reducer 长什么样:

  1. const initialState = {
  2. visibilityFilter : 'SHOW_ALL',
  3. todos : []
  4. };
  5. function appReducer(state = initialState, action) {
  6. switch(action.type) {
  7. case 'SET_VISIBILITY_FILTER' : {
  8. return Object.assign({}, state, {
  9. visibilityFilter : action.filter
  10. });
  11. }
  12. case 'ADD_TODO' : {
  13. return Object.assign({}, state, {
  14. todos : state.todos.concat({
  15. id: action.id,
  16. text: action.text,
  17. completed: false
  18. })
  19. });
  20. }
  21. case 'TOGGLE_TODO' : {
  22. return Object.assign({}, state, {
  23. todos : state.todos.map(todo => {
  24. if (todo.id !== action.id) {
  25. return todo;
  26. }
  27. return Object.assign({}, todo, {
  28. completed : !todo.completed
  29. })
  30. })
  31. });
  32. }
  33. case 'EDIT_TODO' : {
  34. return Object.assign({}, state, {
  35. todos : state.todos.map(todo => {
  36. if (todo.id !== action.id) {
  37. return todo;
  38. }
  39. return Object.assign({}, todo, {
  40. text : action.text
  41. })
  42. })
  43. });
  44. }
  45. default : return state;
  46. }
  47. }

这个函数非常短,但已经开始变得比较复杂。我们在处理两个不同的区域(filtering 和 todo 列表),嵌套使得更新逻辑难以阅读,并且会让我们不清楚到底是什么跟什么。

提取工具函数(Extracting Utility Functions)

第一步是写一个返回更新了相应区域的新对象。这儿还有一个重复的逻辑是在更新数组中的特定项目,我们也可以将他提成一个函数。

  1. function updateObject(oldObject, newValues) {
  2. // 用空对象作为第一个参数传递给 Object.assign,以确保是复制数据,而不是去改变原来的数据
  3. return Object.assign({}, oldObject, newValues);
  4. }
  5. function updateItemInArray(array, itemId, updateItemCallback) {
  6. const updatedItems = array.map(item => {
  7. if(item.id !== itemId) {
  8. // 因为我们只想更新一个项目,所以保留所有的其他项目
  9. return item;
  10. }
  11. // 使用提供的回调来创建新的项目
  12. const updatedItem = updateItemCallback(item);
  13. return updatedItem;
  14. });
  15. return updatedItems;
  16. }
  17. function appReducer(state = initialState, action) {
  18. switch(action.type) {
  19. case 'SET_VISIBILITY_FILTER' : {
  20. return updateObject(state, {visibilityFilter : action.filter});
  21. }
  22. case 'ADD_TODO' : {
  23. const newTodos = state.todos.concat({
  24. id: action.id,
  25. text: action.text,
  26. completed: false
  27. });
  28. return updateObject(state, {todos : newTodos});
  29. }
  30. case 'TOGGLE_TODO' : {
  31. const newTodos = updateItemInArray(state.todos, action.id, todo => {
  32. return updateObject(todo, {completed : !todo.completed});
  33. });
  34. return updateObject(state, {todos : newTodos});
  35. }
  36. case 'EDIT_TODO' : {
  37. const newTodos = updateItemInArray(state.todos, action.id, todo => {
  38. return updateObject(todo, {text : action.text});
  39. });
  40. return updateObject(state, {todos : newTodos});
  41. }
  42. default : return state;
  43. }
  44. }

这样就减少了重复,使得代码的可读性更高。

提取 case reducer

接下来,把特殊逻辑封装成对应的函数:

  1. // 省略了内容
  2. function updateObject(oldObject, newValues) {}
  3. function updateItemInArray(array, itemId, updateItemCallback) {}
  4. function setVisibilityFilter(state, action) {
  5. return updateObject(state, {visibilityFilter : action.filter });
  6. }
  7. function addTodo(state, action) {
  8. const newTodos = state.todos.concat({
  9. id: action.id,
  10. text: action.text,
  11. completed: false
  12. });
  13. return updateObject(state, {todos : newTodos});
  14. }
  15. function toggleTodo(state, action) {
  16. const newTodos = updateItemInArray(state.todos, action.id, todo => {
  17. return updateObject(todo, {completed : !todo.completed});
  18. });
  19. return updateObject(state, {todos : newTodos});
  20. }
  21. function editTodo(state, action) {
  22. const newTodos = updateItemInArray(state.todos, action.id, todo => {
  23. return updateObject(todo, {text : action.text});
  24. });
  25. return updateObject(state, {todos : newTodos});
  26. }
  27. function appReducer(state = initialState, action) {
  28. switch(action.type) {
  29. case 'SET_VISIBILITY_FILTER' : return setVisibilityFilter(state, action);
  30. case 'ADD_TODO' : return addTodo(state, action);
  31. case 'TOGGLE_TODO' : return toggleTodo(state, action);
  32. case 'EDIT_TODO' : return editTodo(state, action);
  33. default : return state;
  34. }
  35. }

现在很清楚每个 case 发生了什么。我们也可以看到一些模式的雏形。

按域拆分数据(Separating Data Handling by Domain)

目前的 Reducer 仍然需要关心程序中所有不同的 case。下面尝试把 filter 逻辑和 todo 逻辑分离:

  1. // 省略了内容
  2. function updateObject(oldObject, newValues) {}
  3. function updateItemInArray(array, itemId, updateItemCallback) {}
  4. function setVisibilityFilter(visibilityState, action) {
  5. // 从技术上将,我们甚至不关心之前的状态
  6. return action.filter;
  7. }
  8. function visibilityReducer(visibilityState = 'SHOW_ALL', action) {
  9. switch(action.type) {
  10. case 'SET_VISIBILITY_FILTER' : return setVisibilityFilter(visibilityState, action);
  11. default : return visibilityState;
  12. }
  13. };
  14. function addTodo(todosState, action) {
  15. const newTodos = todosState.concat({
  16. id: action.id,
  17. text: action.text,
  18. completed: false
  19. });
  20. return newTodos;
  21. }
  22. function toggleTodo(todosState, action) {
  23. const newTodos = updateItemInArray(todosState, action.id, todo => {
  24. return updateObject(todo, {completed : !todo.completed});
  25. });
  26. return newTodos;
  27. }
  28. function editTodo(todosState, action) {
  29. const newTodos = updateItemInArray(todosState, action.id, todo => {
  30. return updateObject(todo, {text : action.text});
  31. });
  32. return newTodos;
  33. }
  34. function todosReducer(todosState = [], action) {
  35. switch(action.type) {
  36. case 'ADD_TODO' : return addTodo(todosState, action);
  37. case 'TOGGLE_TODO' : return toggleTodo(todosState, action);
  38. case 'EDIT_TODO' : return editTodo(todosState, action);
  39. default : return todosState;
  40. }
  41. }
  42. function appReducer(state = initialState, action) {
  43. return {
  44. todos : todosReducer(state.todos, action),
  45. visibilityFilter : visibilityReducer(state.visibilityFilter, action)
  46. };
  47. }

我们注意到,两个 reducer 分别关心 state 中的不同的部分。都只需要把自身关心的数据作为参数,不再需要返回复杂的嵌套型 state 对象了,代码变得更简单。

减少样板代码

马上就大功告成了。因为很多人不喜欢使用 switch 这种语法结构,创建一个 action 到 case 查找表示非常通用的做法。可以使用 缩减样板代码 中提到的 createReducer 函数减少样板代码。

  1. // 省略了内容
  2. function updateObject(oldObject, newValues) {}
  3. function updateItemInArray(array, itemId, updateItemCallback) {}
  4. function createReducer(initialState, handlers) {
  5. return function reducer(state = initialState, action) {
  6. if (handlers.hasOwnProperty(action.type)) {
  7. return handlers[action.type](state, action)
  8. } else {
  9. return state
  10. }
  11. }
  12. }
  13. // 省略了内容
  14. function setVisibilityFilter(visibilityState, action) {}
  15. const visibilityReducer = createReducer('SHOW_ALL', {
  16. 'SET_VISIBILITY_FILTER' : setVisibilityFilter
  17. });
  18. // 省略了内容
  19. function addTodo(todosState, action) {}
  20. function toggleTodo(todosState, action) {}
  21. function editTodo(todosState, action) {}
  22. const todosReducer = createReducer([], {
  23. 'ADD_TODO' : addTodo,
  24. 'TOGGLE_TODO' : toggleTodo,
  25. 'EDIT_TODO' : editTodo
  26. });
  27. function appReducer(state = initialState, action) {
  28. return {
  29. todos : todosReducer(state.todos, action),
  30. visibilityFilter : visibilityReducer(state.visibilityFilter, action)
  31. };
  32. }

通过切片组合 Reducer(Combining Reducers by Slice)

最后一步了,使用 Redux 中 combineReducers 这个工具函数去把管理每个 state 切片的逻辑组合起来,形成顶层的 reducer。最终变成这样:

  1. // 可重用的工具函数
  2. function updateObject(oldObject, newValues) {
  3. // 将空对象作为第一个参数传递给 Object.assign,以确保只是复制数据,而不是去改变数据
  4. return Object.assign({}, oldObject, newValues);
  5. }
  6. function updateItemInArray(array, itemId, updateItemCallback) {
  7. const updatedItems = array.map(item => {
  8. if(item.id !== itemId) {
  9. // 因为我们只想更新一个项目,所以保留所有的其他项目
  10. return item;
  11. }
  12. // 使用提供的回调来创建新的项目
  13. const updatedItem = updateItemCallback(item);
  14. return updatedItem;
  15. });
  16. return updatedItems;
  17. }
  18. function createReducer(initialState, handlers) {
  19. return function reducer(state = initialState, action) {
  20. if (handlers.hasOwnProperty(action.type)) {
  21. return handlers[action.type](state, action)
  22. } else {
  23. return state
  24. }
  25. }
  26. }
  27. // 处理特殊 case 的 Handler ("case reducer")
  28. function setVisibilityFilter(visibilityState, action) {
  29. // 从技术上将,我们甚至不关心之前的状态
  30. return action.filter;
  31. }
  32. // 处理整个 state 切片的 Handler ("slice reducer")
  33. const visibilityReducer = createReducer('SHOW_ALL', {
  34. 'SET_VISIBILITY_FILTER' : setVisibilityFilter
  35. });
  36. // Case reducer
  37. function addTodo(todosState, action) {
  38. const newTodos = todosState.concat({
  39. id: action.id,
  40. text: action.text,
  41. completed: false
  42. });
  43. return newTodos;
  44. }
  45. // Case reducer
  46. function toggleTodo(todosState, action) {
  47. const newTodos = updateItemInArray(todosState, action.id, todo => {
  48. return updateObject(todo, {completed : !todo.completed});
  49. });
  50. return newTodos;
  51. }
  52. // Case reducer
  53. function editTodo(todosState, action) {
  54. const newTodos = updateItemInArray(todosState, action.id, todo => {
  55. return updateObject(todo, {text : action.text});
  56. });
  57. return newTodos;
  58. }
  59. // Slice reducer
  60. const todosReducer = createReducer([], {
  61. 'ADD_TODO' : addTodo,
  62. 'TOGGLE_TODO' : toggleTodo,
  63. 'EDIT_TODO' : editTodo
  64. });
  65. // 顶层 reducer
  66. const appReducer = combineReducers({
  67. visibilityFilter : visibilityReducer,
  68. todos : todosReducer
  69. });

现在我们有了分离集中 reducer 的例子:像 updateObjectcreateReducer 一样的工具函数,像 setVisibilityFilteraddTodo 一样的处理器(Handler),像 visibilityReducertodosReducer 一样的处理单个切片数据的 Handler。appReducer 可以被当作是顶层 reducer。

这个例子中最后的结果看上去比原始的版本更长,这主要是因为工具函数的提取,注释的添加和一些为了清楚起见的故意冗长(比如单独的 return 语句)。单独的看每个功能,他们承担的责任更小,意图也更加清楚。在真正的应用中,这些函数将会分到单独的文件中,比如:reducerUtilities.jsvisibilityReducer.jstodosReudcer.jsrootReducer.js