主要组成

通过前面的介绍可以发现 Redux 应用的主要组成有 action、reducer 和 store。下面借助 todos 这个示例详细地绍这三部分。

action

action 是 Redux 中信息的载体,是 store 唯一的信息来源。把 action 发送给 store 必须通过 store 的 dispatch 方法。action 是普通的 JavaScript 对象,但每个 action 必须有一个 type 属性描述 action 的类型,type 一般被定义为字符串常量。除了 type 属性外,action 的结构完全由自己决定,但应该确保 action 的结构能清晰地描述实际业务场景。一般通过 action creator 创建 action,action creator 是返回 action 的函数。例如,下面是一个新增待办事项的 action creator:

function addTodo(text) {
    return {
        type: 'ADD_TODO',
        text
    }
}

todos 应用涉及的操作有新增待办事项、修改待办事项的状态(已完成/未完成)、筛选当前显示的待办事项列表。对应的完整 action creator 如下:

// actions.js

// action types
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'

// 筛选待办事项列表的条件
export const VisibilityFilters = {
    SHOW_ALL: 'SHOW_ALL',
    SHOW_COMPLETED: 'SHOW_COMPLETED',
    SHOW_ACTIVE: 'SHOW_ACTIVE'
}

// action creators
// 新增待办事项
export function addTodo(text) {
    return { type: ADD_TODO, text}
}

// 修改某个待办事项的状态,index 是待办事项在 todos 数组中的位置索引
export function toggleTodo(index) {
    return { type: TOGGLE_TODO, index }
}

// 筛选当前显示的待办事项列表
export function setVisiblilityFilter(filter) {
    return { type: SET_VISIBILITY_FILTER, filter }
}

reducer

action 用于描述应用发生了什么操作,reducer 则根据 action 做出响应,决定如何修改应用的状态 state。既然是修改 state,那么就应该在编写 reducer 前设计好 state。state 既可以包含服务器端获取的数据,也可以包含 UI 状态,前面已经设计了 todos 应用的 state:

{
  "todos": [{
    "text": 'Learn React',
    "completed": true
  }, {
    "text": 'Learn Redux',
    "completed": false
  }],
  "visibilityFilter": 'SHOW_COMPLETED'
}

有了 state,我们再为 state 编写 reducer。reducer 是一个纯函数,它接收两个参数,当前的 state 和 action,返回新的 state。reducer 函数签名如下:

(previousState, action) => newState

我们先来创建一个最基本的 reducer:

import { VisibilityFilters } from './actions'

const initialState = {
    todos: [],
    visibilityFilter: VisibilityFilters.SHOW_ALL
}

// reducer
function todoApp(state = initialState, action) {
    return state
}

todoApp 这个 reducer 不做任何事情,对于任意 action 做出的响应都是直接返回前一个 state。这里需要注意 state 初始值的设置,当 todoApp 第一次被调用时,state 等于 undefined,这时会用 initialState 初始化 state。现在为 todoApp 添加处理 type 等于 SET_VISIBILITY_FILTER 的 action,要做的事情是改变 state 的 visibilityFilter:

function todoApp(state = initialState, action) {
    switch (action.type) {
        case SET_VISIBILITY_FILTER:
            return { ...state, visibilityFilter: action.filter }
        default:
            return state
    }
}

注意,这里使用 ES6 的扩展运算符(…​)创建新的 state 对象,避免直接修改之前的 state 对象。还有一种常见的写法是使用 ES6 的 Object.assign() 函数:

function todoApp(state = initialState, action) {
    switch (action.type) {
        case SET_VISIBILITY_FILTER:
            return Object.assign({}, state, {
                visibilityFilter: action.filter
            })
        default:
            return state
    }
}

下面再来处理另外两个 action,同样需要保证每次返回的 state 对象都是一个新的对象:

function todoApp(state = initialState, action) {
    switch (action.type) {
        case SET_VISIBILITY_FILTER:
            return Object.assign({}, state, {
                visibilityFilter: action.filter
            })
        // 新增待办事项
        case ADD_TODO:
            // 使用了 ES6 的扩展语法
            return { ...state,
                todos: [
                    ...state.todos,
                    {
                        text: action.text,
                        completed: false
                    }
                ]
            }
        // 修改待办事项的状态(已完成/未完成)
        case TOGGLE_TODO:
            return { ...state,
                todos: state.todos.map((todo, index) => {
                    if(index === action.index) {
                        return { ...todo, completed: !todo.completed }
                    }
                    return todo
                })
            }
        default:
            return state
    }
}

当前,我们使用 todoApp 一个 reducer 处理所有的 action,当应用变得复杂时,这个 reducer 也会逐渐变复杂,这时,一般会拆分出多个 reducer,每个 reducer 处理 state 中的部分状态。例如,这里可以拆分出 todos 和 visibilityFilter 两个 reducer,分别处理 state 的 todos 和 visibilityFilter 两个子状态:

function todos(state = [], action) {
  switch (action.type) {
    case 'ADD_TODO':
      return state.concat([{ text: action.text, completed: false }])
    case 'TOGGLE_TODO':
      return state.map((todo, index) =>
        action.index === index
          ? {...todo, completed: !todo.completed}
          : todo
      )
    default:
      return state
  }
}

// 处理 visibilityFilter 的 reducer
function visibilityFilter(state = 'SHOW_ALL', action) {
  switch (action.type) {
    case 'SET_VISIBILITY_FILTER':
      return action.filter
    default:
      return state
  }
}

// todoApp 简化为:
function todoApp(state = {}, action) {
    return {
        todos: todos(state.todos, action),
        visibilityFilter: visibilityFilter(state.visibilityFilter, action)
    }
}

注意,每个拆分的 reducer 只接收它负责的 state 中的部分属性,而不再是完整的 state 对象。todos 接收 state.todos,visibilityFilter 接收 state.visibilityFilter。这样,当应用较复杂时,就可以拆分出多个 reducer 保存到独立的文件中。

Redux 还提供了一个 combineReducers 函数,用于合并多个 reducer。使用 combineReducers,todoApp 可以改写如下:

import { combineReducers } from 'redux'

const todoApp = combineReducers({
    todos,
    visibilityFilter
})

它等价于:

function todoApp(state = {}, action) {
    return {
        todos: todos(state.todos, action),
        visibilityFilter: visibilityFilter(state.visibilityFilter, action)
    }
}

还可以为 combineReducers 接收的参数对象指定和 reducer 的函数名不同的 key 值:

const reducer = combineReducers({
    a: doSomethingWithA,
    b: processB,
    c: c
})

它等价于:

function reducer(state = {}, action) {
    return {
        a: doSomethingWithA(state.a, action),
        b: processB(state.b, action),
        c: c(state.c, action)
    }
}

可见,combineReducers 传递给每个 reducer 的 state 中的属性取决于它的参数对象的 key 值。

store

store 是 Redux 中的一个对象,也是 action 和 reducer 之间的桥梁。store 主要负责以下几个工作:

  1. 保存应用状态。

  2. 通过方法 getState() 访问应用状态。

  3. 通过方法 dispatch(action) 发送更新状态的意图。

  4. 通过方法 subscribe(listener) 注册监听函数、监听应用状态的改变。

一个 Redux 应用中只有一个 store,store 保存了唯一数据源。store 通过 createStore() 函数创建,创建时需要传递 reducer 作为参数,创建 todos 应用的 store 的代码如下:

import { createStore } from 'redux'
import todoApp from './reducers'

let stroe = createStore(todoApp)

创建 store 时还可以设置应用的初始状态:

// initialState 代表初始状态
let store = createStore(todoApp, initialState)

除了可以在创建 store 时设置应用的初始状态外,还可以在创建 reducer 时设置应用的初始状态,例如:

// 初始状态是一个空数组
function todos(state = [], action) {
    //...
}

// 初始状态等于 SHOW_ALL
function visibilityFilter(state = 'SHOW_ALL', action) {
    //...
}

todos 设置的初始状态是 state = [],visibilityFilter 设置的初始状态是 state ='SHOW_ALL',这样,当把这两个 reducer 合并成一个 reducer 时,两个 reducer 的初始状态就构成了整个应用的初始状态:

{
  todos: [],
  visibilityFilter: 'SHOW_ALL'
}

store 创建完成后,就可以通过 getState() 获取当前应用的状态 state:

const state = store.getState()

当需要修改 state 时,通过 store 的 dispatch 方法发送 action。例如,发送一个新增待办事项的 action:

// 定义 action
function addTodo(text) {
    return {type: 'ADD_TODO', text}
}

// 发送 action
store.dispatch(addTodo('Learn about actions'))

当 todoApp 这个 reducer 处理完成 addTodo 这个 action 时,应用的状态会被更新,此时通过 store.getState() 可以得到最新的应用状态。为了能准确知道应用状态更新的时间,需要向 store 注册一个监听函数:

let unsubscribe = store.subscribe(() =>
    console.log(store.getState())
)

这样,每当应用状态更新时,最新的应用状态就会被打印出来。当需要取消监听时,直接调用 store.subscribe 返回的函数即可:

unsubscribe()

下面再来总结一下 Redux 的数据流过程。

  1. 调用 store.dispatch(action)。一个 action 是一个用于描述 “发生了什么” 的对象。store.dispatch(action) 可以在应用的任何地方调用,包括组件、XHR 的回调,甚至在定时器中。

  2. Redux 的 store 调用 reducer 函数。store 传递两个参数给 reducer:当前应用的状态和 action。reducer 必须是一个纯函数,它的唯一职责是计算下一个应用的状态。

  3. 根 reducer 会把多个子 reducer 的返回结果组合成最终的应用状态。根 reducer 的构建形式完全取决于用户。Redux 提供了 combineReducers,方便把多个拆分的子 reducer 组合到一起,但完全可以不使用它。当使用 combineReducers 时,action 会传递给每一个子 reducer 处理,子 reducer 处理后的结果会合并成最终的应用状态。

  4. Redux 的 store 保存根 reducer 返回的完整应用状态。此时,应用状态才完成更新。如果 UI 需要根据应用状态进行更新,那么这就是更新 UI 的时机。对于 React 应用而言,可以在这个时候调用组件的 setState 方法,根据新的应用状态更新 UI。