Redux 基础教程 文档翻译 一 action reducer store

基础部分 一

译者目前在做前端,博客内容主要是文档翻译。如果读者希望我翻译某些和前端相关的文档,欢迎留言告诉我。对于已有在网络上能够搜到质量较高译文的文章,我就不做重复工作了。本人精力有限,翻译质量达不到出版书籍的程度,可能有些人看不懂,不过我相信这总会帮助到一些人。有空的时候我会返回来对之前的文章进行润色。

对应官方文档,基础部分,Actions,Reducers,Store三节。

https://redux.js.org/basics

文内索引:

  1. Actions
  2. Reducers
  3. Store

Actions

首先,让我们定义一些action。

action是从你的应用发送到store的数据的信息负载。它们是store中唯一的信息源。你可以使用store.dispatch()将它们发送到store。

下面是一个action的示例,它会添加一个代办事项。

const ADD_TODO = 'ADD_TODO'

{
  type: ADD_TODO,
  text: 'Build my first Redux app'
}

action是简单的JavaScript对象。action必须有一个type(类型)属性来标示什么type的action将被执行。type通常被定义为字符串常量。当你的应用足够大时,需要把它们放入不同的模块(module)。

import { ADD_TODO, REMOVE_TODO } from '../actionTypes'

你不是一定要将action type的定义放入不同的文件,甚至根本不需要定义它们。对于一个小项目,可以简单的用字符串字面量来定义action type。然而,在大型的代码库中,声明常量是有益处的。读Reducing Boilerplate来获得关于保持代码库干净的建议。

除了type,action对象的构成取决于你。如果你感兴趣,读Flux Standard Action来获得一些如何构建action对象的建议。

我们添加一个action来表示用户将待办事项设置为完成状态。我们通过index来引用一个具体的待办事项,因为它们被存储在数组里。在真实的应用中,明智的做法是创建一个唯一的ID来进行标记。

{
  type: TOGGLE_TODO,
  index: 5
}

建议在action中传递尽量少的信息。例如,传递index比传递整个对象要好。

最后,再设置一个action来改变待办事项的可见性。

{
  type: SET_VISIBILITY_FILTER,
  filter: SHOW_COMPLETED
}

action creator

action creator的功能就是生成action。很容易将术语action和action creator搞混,所以要尽量区分它们。

在Redux中,action creator简单地返回一个action:

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

这使得它们简单并易于测试。

传统的Flux中,action creator在被唤醒时经常会触发一个dispatch,例如:

function addTodoWithDispatch(text) {
  const action = {
    type: ADD_TODO,
    text
  }
  dispatch(action)
}

在Redux中这不是惯例。

取而代之的是使用返回的结果来触发dispatch():

dispatch(addTodo(text))
dispatch(completeTodo(index))

为了简化,你可以创建一个bound action creator,它会自动进行dispatch:

const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))

现在你可以直接执行:

boundAddTodo(text)
boundCompleteTodo(index)

dispatch()函数可以直接从store.dispatch()访问,但是更多时候你会使用react-redux的connect()。你可以使用bindActionCreators()来自动将一些action creator和dispatch()函数绑定。

action creator还可以是异步的并且具有副作用。你可以读[进阶教程(之后会补充翻译)](advanced tutorial)中的异步action来学习如何处理AJAX响应和在异步工作流中创建action creator。在完成基础教程前不要跳到异步action,因为这里包含了其他的必要知识。

源代码

actions.js
/*
 * action types
 */
​
export const ADD_TODO = 'ADD_TODO'
export const TOGGLE_TODO = 'TOGGLE_TODO'
export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'/*
 * other constants
 */
​
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 }
}
​
export function toggleTodo(index) {
  return { type: TOGGLE_TODO, index }
}
​
export function setVisibilityFilter(filter) {
  return { type: SET_VISIBILITY_FILTER, filter }
}

Reducers

reducer指出了应用的状态如何根据actionfan发送到store的响应而改变。记住,action仅仅描述了发生了什么,但没有描述应用的状态如何改变。

设计State Shape

在Redux中,所有应用状态被存储在单一的对象中。建议在写代码前设计好state的结构。比如如果用最小的对象来表述应用状态?

对于我们的待变事项应用,我们想要存储两个不同的东西:

  • 当前选择的可见性
  • 当前的代办事项列表

你经常会发现你需要存储一些信息,例如UI状态。这是正常的,但是要将数据和UI状态分离。

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

在更复杂的应用中,你将会需要不同的实体间互相引用。我们建议实体尽可能单一,不要任何的嵌入。让每一个对象保存的实体具有一个作为key的ID,并且使用ID来进行引用,包括在列表中。将应用的状态想象成数据库。这个方式在normalizr的文档中有对细节的讨论。例如,todosById: { id -> todo }todos: array<id>是对于真实的应用建议的用法,但在教程中我们会尽量保持示例简单(而不这么做)。

处理action

现在我们已经决定了state的结构,我们准备好为它写一个reducer了。reducer是一个纯函数,根据之前的state和action来返回下一个state。

(previousState, action) => newState

它之所以称为reducer是因为函数的类型可以被传递入Array.prototype.reduce(reducer, ?initialValue)。让reducer保持纯函数非常重要。永远不要在reducer里面做下面的事:

  • 修改它的参数
  • 执行有副作用的功能,如调用API和改变路由。
  • 调用任何非纯函数,如Date.now()Math.random()

我们将在进阶指南中讨论如何执行带有副作用的功能。现在,仅仅记住reducer必须是纯函数就行了。给出同样的参数,返回同样的结果。没有副作用。没有修改。仅仅是计算。

带着这些原则,我们开始写reducer,让他逐步理解之前定义的action。

首先指定初始化的state。Redux第一次会使用undefined状态来调用reducer。此时可以返回应用的初始state:

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。它需要做的事处理state中的visibilityFilter。很简单:

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

注意:

  1. 我们不改变state参数。我们通过Object.assign()来创建副本。Object.assign(state, { visibilityFilter: action.filter })也是错的,它修改了第一个参数。你一定要将空对象作为第一个参数。你还可以使用对象展开操作符来实现:{ ...state, ...newState }
  2. default时返回之前的state。对于未知的action返回之前的state很重要。

Object.assign()是ES6的一部分,老的浏览器不支持。为了支持它们,你需要使用polyfill,Babel插件或其它的工具库如_.assign()

switch语句不是官方的做法。Flux的官方做法是:需要触发一个更新(update),dispatcher需要注册一个state,store需要是一个对象(当你想要一个universal app的时候会发生困难)。Redux通过使用纯reducer替代事件触发器解决了这些问题。

不幸的是一些人根据文档中是否使用switch语句来选择框架。如果你不喜欢switch语句,你可以使用自定义的createReducer函数,它接受一个处理器(handler)映射(map),参考“reducing boilerplate

处理更多action

我们还有两个action要处理。就像我们对SET_VISIBILITY_FILTER做的,我们将引入ADD_TODOTOGGLE_TODO action并且扩展reducer来处理ADD_TODO

import {
  ADD_TODO,
  TOGGLE_TODO,
  SET_VISIBILITY_FILTER,
  VisibilityFilters
} 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末尾拼接了一个新的条目。新的条目使用了action中的数据。

最后,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
    })
  })

因为我们想更新一个特定的条目而不对它本身做改变,我们必须对除index所在元素外的其他元素创建一个新的数组。如果你经常写这种操作,建议使用immutability-helperupdeep这样的工具或Immutable这样支持深度更新的库。记住不要修改state中的东西除非你对它进行了克隆。

分割reducer

这是迄今为止的代码,它非常冗长:

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完全独立地被更新。有时state的内容互相依赖,这时需要更多的考虑,但在我们的示例中可以轻易地将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:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    case TOGGLE_TODO:
      return Object.assign({}, state, {
        todos: todos(state.todos, action)
      })
    default:
      return state
  }
}

注意todos仍然接受state,但它是个数组!现在todoApp将某些状态交给它管理,并且todos知道如何处理这些状态。这称为reducer composition,并且是构建Redux应用的基础模式。

让我们继续讨论reducer composition。我们可以将visibilityFilter分离吗?是的。

我们用ES6对象解构来声明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,让它调用管理state不同部分的reducer,并且将它们整合进一个单独对象中。这不再需要了解完整的初始state。当state是undefined时,各个reducer会返回初始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
  }
}
​
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的一部分。每一个reducer的state参数是不同的,并且和它所管理的那部分state相一致。

这已经看起来不错了。当应用很大时,我们可以将reducer分离进不通的文件并且保持它们独立,并且管理不同的数据区域。

最后,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)
  }
}

你同样可以为它们提供不同的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()仅仅生成一个函数,通过key来分割state并调用你的reducer,并且再次将结果整合为一个对象。这不是magic。就像其他的reducer,combineReducers()不会创建新对象如果所有reducer都没有改变状态。

因为combineReducers需要一个对象,我们可以将顶级reducer分割到不同文件中,export每一个reducer函数,并且使用import * as reducers获得它们的名字作为key的对象。

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

因为import *是新的语法,我们不会再在文档中使用它以避免混乱,但是你肯能在一些社区的示例中碰到它。

源代码

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

Store

在之前的章节中,我们定义了action来表示“发生了什么”并且定义了reducer来根据action更新state。

store将它们放到一起,store负责:

注意在Redux应用中你只需要一个store。当你需要分割你的数据处理时,使用reducer composition而不是多个store。

如果你有一个reducer,那么创建store是很简单的。在之前的章节中,我们使用combineReducers()将多个reducer整合进一个中。我们现在在如它,并将它传入createStore()

import { createStore } from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)

你还可以将初始化状态作为第二个参数传入createStore()。这很容易将客户端的state和服务器端的Redux应用状态同步。

Dispatching Actions

现在我们创建了一个store,让我们确认程序工作一切正常。即使没有UI,我们也可以测试更新的逻辑。

import {
  addTodo,
  toggleTodo,
  setVisibilityFilter,
  VisibilityFilters
} from './actions'// Log the initial state
console.log(store.getState())
​
// Every time the state changes, log it
// Note that subscribe() returns a function for unregistering the listener
const unsubscribe = store.subscribe(() =>
  console.log(store.getState())
)
​
// Dispatch some actions
store.dispatch(addTodo('Learn about actions'))
store.dispatch(addTodo('Learn about reducers'))
store.dispatch(addTodo('Learn about store'))
store.dispatch(toggleTodo(0))
store.dispatch(toggleTodo(1))
store.dispatch(setVisibilityFilter(VisibilityFilters.SHOW_COMPLETED))
​
// Stop listening to state updates
unsubscribe()

你可看到store中state的变化。

图

我们设定了应用的行为,即使没有开始写UI。我们不会再这个教程中做这件事,但你可以自己来写。因为(reducer)是纯函数,所以你不需要模拟任何(环境)。调用它们然后会返回值进行断言就可以了。

源代码

index.js
import { createStore } from 'redux'
import todoApp from './reducers'
​
const store = createStore(todoApp)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值