• Reducer
    • 设计 State 结构
      • 处理 Reducer 关系时的注意事项
  • Action 处理
    • Object.assign 须知
    • switch 和样板代码须知
  • 处理多个 action
  • 拆分 Reducer
    • ES6 用户使用注意
  • 源码
    • reducers.js
  • 下一步

    Reducer

    Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。

    设计 State 结构

    在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?

    以 todo 应用为例,需要保存两种不同的数据:

    • 当前选中的任务过滤条件;
    • 完整的任务列表。

    通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。

    1. {
    2. visibilityFilter: 'SHOW_ALL',
    3. todos: [
    4. {
    5. text: 'Consider using Redux',
    6. completed: true,
    7. },
    8. {
    9. text: 'Keep all state in a single tree',
    10. completed: false
    11. }
    12. ]
    13. }
    处理 Reducer 关系时的注意事项

    开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 todosById: { id -> todo }todos: array<id> 是比较好的方式,本文中为了保持示例简单没有这样处理。

    Action 处理

    现在我们已经确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。

    1. (previousState, action) => newState

    之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:

    • 修改传入参数;
    • 执行有副作用的操作,如 API 请求和路由跳转;
    • 调用非纯函数,如 Date.now()Math.random()

    在高级篇里会介绍如何执行有副作用的操作。现在只需要谨记 reducer 一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。

    明白了这些之后,就可以开始编写 reducer,并让它来处理之前定义过的 action。

    我们将以指定 state 的初始状态作为开始。Redux 首次执行时,state 为 undefined,此时我们可借机设置并返回应用的初始 state。

    1. import { VisibilityFilters } from './actions'
    2. const initialState = {
    3. visibilityFilter: VisibilityFilters.SHOW_ALL,
    4. todos: []
    5. };
    6. function todoApp(state, action) {
    7. if (typeof state === 'undefined') {
    8. return initialState
    9. }
    10. // 这里暂不处理任何 action,
    11. // 仅返回传入的 state。
    12. return state
    13. }

    这里一个技巧是使用 ES6 参数默认值语法 来精简代码。

    1. function todoApp(state = initialState, action) {
    2. // 这里暂不处理任何 action,
    3. // 仅返回传入的 state。
    4. return state
    5. }

    现在可以处理 SET_VISIBILITY_FILTER。需要做的只是改变 state 中的 visibilityFilter

    1. function todoApp(state = initialState, action) {
    2. switch (action.type) {
    3. case SET_VISIBILITY_FILTER:
    4. return Object.assign({}, state, {
    5. visibilityFilter: action.filter
    6. })
    7. default:
    8. return state
    9. }
    10. }

    注意:

    1. 不要修改 state 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。

    2. default 情况下返回旧的 state遇到未知的 action 时,一定要返回旧的 state

    Object.assign 须知

    Object.assign() 是 ES6 特性,但多数浏览器并不支持。你要么使用 polyfill,Babel 插件,或者使用其它库如 _.assign() 提供的帮助方法。

    switch 和样板代码须知

    switch 语句并不是严格意义上的样板代码。Flux 中真实的样板代码是概念性的:更新必须要发送、Store 必须要注册到 Dispatcher、Store 必须是对象(开发同构应用时变得非常复杂)。为了解决这些问题,Redux 放弃了 event emitters(事件发送器),转而使用纯 reducer。

    很不幸到现在为止,还有很多人存在一个误区:根据文档中是否使用 switch 来决定是否使用它。如果你不喜欢 switch,完全可以自定义一个 createReducer 函数来接收一个事件处理函数列表,参照“减少样板代码”。

    处理多个 action

    还有两个 action 需要处理。就像我们处理 SET_VISIBILITY_FILTER 一样,我们引入 ADD_TODOTOGGLE_TODO 两个actions 并且扩展我们的 reducer 去处理 ADD_TODO.

    1. import {
    2. ADD_TODO,
    3. TOGGLE_TODO,
    4. SET_VISIBILITY_FILTER,
    5. VisibilityFilters
    6. } from './actions'
    7. ...
    8. function todoApp(state = initialState, action) {
    9. switch (action.type) {
    10. case SET_VISIBILITY_FILTER:
    11. return Object.assign({}, state, {
    12. visibilityFilter: action.filter
    13. })
    14. case ADD_TODO:
    15. return Object.assign({}, state, {
    16. todos: [
    17. ...state.todos,
    18. {
    19. text: action.text,
    20. completed: false
    21. }
    22. ]
    23. })
    24. default:
    25. return state
    26. }
    27. }

    如上,不直接修改 state 中的字段,而是返回新对象。新的 todos 对象就相当于旧的 todos 在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。

    最后,TOGGLE_TODO 的实现也很好理解:

    1. case TOGGLE_TODO:
    2. return Object.assign({}, state, {
    3. todos: state.todos.map((todo, index) => {
    4. if (index === action.index) {
    5. return Object.assign({}, todo, {
    6. completed: !todo.completed
    7. })
    8. }
    9. return todo
    10. })
    11. })

    我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。(译者注:Javascript中的对象存储时均是由值和指向值的引用两个部分构成。此处突变指直接修改引用所指向的值, 而引用本身保持不变。) 如果经常需要这类的操作,可以选择使用帮助类 React-addons-update,updeep,或者使用原生支持深度更新的库 Immutable。最后,时刻谨记永远不要在克隆 state 前修改它。

    拆分 Reducer

    目前的代码看起来有些冗长:

    1. function todoApp(state = initialState, action) {
    2. switch (action.type) {
    3. case SET_VISIBILITY_FILTER:
    4. return Object.assign({}, state, {
    5. visibilityFilter: action.filter
    6. })
    7. case ADD_TODO:
    8. return Object.assign({}, state, {
    9. todos: [
    10. ...state.todos,
    11. {
    12. text: action.text,
    13. completed: false
    14. }
    15. ]
    16. })
    17. case TOGGLE_TODO:
    18. return Object.assign({}, state, {
    19. todos: state.todos.map((todo, index) => {
    20. if (index === action.index) {
    21. return Object.assign({}, todo, {
    22. completed: !todo.completed
    23. })
    24. }
    25. return todo
    26. })
    27. })
    28. default:
    29. return state
    30. }
    31. }

    上面代码能否变得更通俗易懂?这里的 todosvisibilityFilter 的更新看起来是相互独立的。有时 state 中的字段是相互依赖的,需要认真考虑,但在这个案例中我们可以把 todos 更新的业务逻辑拆分到一个单独的函数里:

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

    注意 todos 依旧接收 state,但它变成了一个数组!现在 todoApp 只把需要更新的一部分 state 传给 todos 函数,todos 函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。

    下面深入探讨一下如何做 reducer 合成。能否抽出一个 reducer 来专门管理 visibilityFilter?当然可以:

    首先引用, 让我们使用 ES6 对象结构 去声明 SHOW_ALL:

    1. const { SHOW_ALL } = VisibilityFilters

    接下来:

    1. function visibilityFilter(state = SHOW_ALL, action) {
    2. switch (action.type) {
    3. case SET_VISIBILITY_FILTER:
    4. return action.filter
    5. default:
    6. return state
    7. }
    8. }

    现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined, 子 reducer 将负责返回它们的默认值。

    1. function todos(state = [], action) {
    2. switch (action.type) {
    3. case ADD_TODO:
    4. return [
    5. ...state,
    6. {
    7. text: action.text,
    8. completed: false
    9. }
    10. ]
    11. case TOGGLE_TODO:
    12. return state.map((todo, index) => {
    13. if (index === action.index) {
    14. return Object.assign({}, todo, {
    15. completed: !todo.completed
    16. })
    17. }
    18. return todo
    19. })
    20. default:
    21. return state
    22. }
    23. }
    24. function visibilityFilter(state = SHOW_ALL, action) {
    25. switch (action.type) {
    26. case SET_VISIBILITY_FILTER:
    27. return action.filter
    28. default:
    29. return state
    30. }
    31. }
    32. function todoApp(state = {}, action) {
    33. return {
    34. visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    35. todos: todos(state.todos, action)
    36. }
    37. }

    注意每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据。

    现在看起来好多了!随着应用的膨胀,我们还可以将拆分后的 reducer 放到不同的文件中, 以保持其独立性并用于专门处理不同的数据域。

    最后,Redux 提供了 combineReducers() 工具类来做上面 todoApp 做的事情,这样就能消灭一些样板代码了。有了它,可以这样重构 todoApp

    1. import { combineReducers } from 'redux'
    2. const todoApp = combineReducers({
    3. visibilityFilter,
    4. todos
    5. })
    6. export default todoApp

    注意上面的写法和下面完全等价:

    1. export default function todoApp(state = {}, action) {
    2. return {
    3. visibilityFilter: visibilityFilter(state.visibilityFilter, action),
    4. todos: todos(state.todos, action)
    5. }
    6. }

    你也可以给它们设置不同的 key,或者调用不同的函数。下面两种合成 reducer 方法完全等价:

    1. const reducer = combineReducers({
    2. a: doSomethingWithA,
    3. b: processB,
    4. c: c
    5. })
    1. function reducer(state = {}, action) {
    2. return {
    3. a: doSomethingWithA(state.a, action),
    4. b: processB(state.b, action),
    5. c: c(state.c, action)
    6. }
    7. }

    combineReducers() 所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。没有任何魔法。正如其他 reducers,如果 combineReducers() 中包含的所有 reducers 都没有更改 state,那么也就不会创建一个新的对象。

    ES6 用户使用注意

    combineReducers 接收一个对象,可以把所有顶级的 reducer 放到一个独立的文件中,通过 export 暴露出每个 reducer 函数,然后使用 import * as reducers 得到一个以它们名字作为 key 的 object:

    1. import { combineReducers } from 'redux'
    2. import * as reducers from './reducers'
    3. const todoApp = combineReducers(reducers)

    由于 import * 还是比较新的语法,为了避免困惑,我们不会在本文档中使用它。但在一些社区示例中你可能会遇到它们。

    源码

    reducers.js

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

    下一步

    接下来会学习 创建 Redux store。store 能维持应用的 state,并在当你发起 action 的时候调用 reducer。