• 使用函数分解(Functional Decomposition)和 Reducer 组合(Reducer Composition)重构 Reducer
    • 初遇 Reducer
    • 提取工具函数(Extracting Utility Functions)
    • 提取 case reducer
    • 按域拆分数据(Separating Data Handling by Domain)
    • 减少样板代码
    • 通过切片组合 Reducer(Combining Reducers by Slice)

    使用函数分解(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