减速器(Reducers)

减速器

操作描述事情发生的事实,但不指定应用程序的状态如何响应。这是减速机的工作。

设计状态形状

在 Redux 中,所有应用程序状态都存储为单个对象。在编写任何代码之前考虑它的形状是个好主意。什么是你的应用程序状态作为对象的最小表示?

对于我们的待办应用程序,我们想要存储两件不同的事情:

  • 当前选定的可见性过滤器;

  • 实际的待办事项列表。

您经常会发现,您需要在状态树中存储一些数据以及一些 UI 状态。这很好,但要尽量将数据与 UI 状态分开。

{ visibilityFilter: 'SHOW_ALL', todos: [ { text: 'Consider using Redux', completed: true, }, { text: 'Keep all state in a single tree', completed: false } ] }

关于关系的注意事项

处理操作

现在我们已经决定了我们的状态对象,我们准备为它编写一个 reducer。reducer 是一个纯函数,它接受之前的状态和一个动作,并返回下一个状态。

(previousState, action) => newState

它被称为 reducer,因为它是你传递给Array.prototype.reduce(reducer, ?initialValue)的函数的类型。减速机保持纯净非常重要。减速机内永远不应该做的事情:

  • 改变论点;

  • 执行 API 调用和路由转换等副作用;

  • 调用非纯函数,例如Date.now()Math.random()

我们将探讨如何在高级演练中执行副作用。现在,请记住减速器必须是纯粹的。给定相同的参数,它应该计算下一个状态并返回它。没有惊喜。没有副作用。没有 API 调用。没有突变。只是一个计算。

有了这个,让我们开始写我们的减速机,逐渐地教它来理解我们之前定义的动作。

我们将首先指定初始状态。Redux 首次用undefined状态调用我们的减速器。这是我们返回应用程序初始状态的机会:

import { VisibilityFilters } from './actions' const initialState = { visibilityFilter: VisibilityFilters.SHOW_ALL, todos: [] } function todoApp(state, action) { if (typeof state === 'undefined') { return initialState } // For now, don't handle any actions // and just return the state given to us. return state }

一个巧妙的技巧是使用 ES6 默认参数语法以更紧凑的方式编写它:

function todoApp(state = initialState, action) { // For now, don't handle any actions // and just return the state given to us. return state }

现在让我们来处理SET_VISIBILITY_FILTER。它所需要做的就是改变visibilityFilter状态。简单来说:

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

注意:

  • 我们不会改变 state**我们创建一个Object.assign()副本。Object.assign(state, { visibilityFilter: action.filter })也是错的:它会改变第一个参数。您必须**提供一个空对象作为第一个参数。您也可以启用对象扩展运算符提议{ ...state, ...newState }来代替写入。

2. 我们回到以前state的default情况。对于任何未知的操作返回state前一个都很重要。

注意Object.assign

处理更多操作

我们还有两个行动来处理!就像我们所做的SET_VISIBILITY_FILTER那样,我们将导入ADD_TODOTOGGLE_TODO操作,然后扩展我们的 reducer 来处理ADD_TODO

import { VisibilityFilters, ADD_TODO, TOGGLE_TODO } from './actions' ... function todoApp(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, { text: action.text, completed: false } ] }) default: return state } }

就像以前一样,我们绝不会直接写入state或其字段,而是返回新的对象。新todos的等于旧的todos与最后一个新的项目连接在一起。新鲜的待办事项是使用行动中的数据构建的。

最后,TOGGLE_TODO处理程序的实现不应该是一个完全惊喜:

case TOGGLE_TODO: return Object.assign{}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign{}, todo, { completed: !todo.completed }) } return todo }) })

因为我们想更新数组中的某个特定项目而不使用突变,所以我们必须创建一个新数组,其中除了索引处的项目外,其他项目都是相同的。如果你发现自己经常编写这样的操作,最好使用像不可变性助手,updeep 的助手,或者像 Immutable 这样的库,它具有对深度更新的本机支持。请记住,除非您先复制它,否则绝对不要指定任何state内容。

分裂减速器

这是我们的代码。它比较冗长:

function todoApp(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, { text: action.text, completed: false } ] }) case TOGGLE_TODO: return Object.assign{}, state, { todos: state.todos.map((todo, index) => { if (index === action.index) { return Object.assign{}, todo, { completed: !todo.completed }) } return todo }) }) default: return state } }

有没有办法让它更容易理解?它看起来像todosvisibilityFilter完全独立更新。有时状态字段彼此依赖,需要更多的考虑,但在我们的例子中,我们可以很容易地将更新todos分成单独的函数:

function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign{}, todo, { completed: !todo.completed }) } return todo }) default: return state } } function todoApp(state = initialState, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return Object.assign{}, state, { visibilityFilter: action.filter }) case ADD_TODO: case TOGGLE_TODO: return Object.assign{}, state, { todos: todos(state.todos, action) }) default: return state } }

请注意,todos也接受state- 但它是一个数组!现在todoApp只是给它一个管理状态的切片,并且todos知道如何更新该切片。这就是所谓的 减速器组成,它是构建Redux应用程序的基本模式。

我们来更多地探讨减速器的组成。我们是否也可以提取减速器管理visibilityFilter?我们可以。

在我们的导入下面,让我们使用ES6 Object Destructuring来声明SHOW_ALL

const { SHOW_ALL } = VisibilityFilters

然后:

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

现在我们可以将主 Reducer 重写为一个函数,该函数调用管理该状态部分的 reducer,并将它们合并为一个对象。它也不需要知道完整的初始状态了。刚开始undefined给子减压器返回初始状态就足够了。

function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign{}, todo, { completed: !todo.completed }) } return todo }) default: return state } } function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todoApp(state = {}, action) { return { visibilityFilter: visibilityFilter(state.visibilityFilter, action), todos: todos(state.todos, action) } }

请注意,这些减速器中的每一个都在管理其全部状态的一部分。 每个 reducer 的state参数都不相同,并且对应于它所管理的状态的一部分。

这已经很好看了!当应用程序较大时,我们可以将减速器拆分为单独的文件并使其完全独立并管理不同的数据域。

最后,Redux 提供了一个名为combineReducers()的实用程序,它执行与todoApp上面目前相同的样板逻辑。在它的帮助下,我们可以像这样重写todoApp

import { combineReducers } from 'redux' const todoApp = combineReducers{ visibilityFilter, todos }) export default todoApp

请注意,这相当于:

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

你也可以给他们不同的键,或以不同的方式调用函数。这两种写联合减速器的方法是等价的:

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()功能都会生成一个函数,根据其按键选择所选状态的切片并将其结果再次组合到单个对象中。这不是魔术。和其他减速器一样,如果向其提供的所有减速器不改变状态,则combineReducers()不会创建新的对象。

ES6 精明用户注意

源代码

reducers.js

import { combineReducers } from 'redux' import { ADD_TODO, TOGGLE_TODO, SET_VISIBILITY_FILTER, VisibilityFilters } from './actions' const { SHOW_ALL } = VisibilityFilters function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter default: return state } } function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] case TOGGLE_TODO: return state.map((todo, index) => { if (index === action.index) { return Object.assign{}, todo, { completed: !todo.completed }) } return todo }) default: return state } } const todoApp = combineReducers{ visibilityFilter, todos }) export default todoApp

下一步

接下来,我们将探讨如何创建一个 Redux 存储来保存状态,并在分派操作时负责调用您的 reducer。