基础部分 一
译者目前在做前端,博客内容主要是文档翻译。如果读者希望我翻译某些和前端相关的文档,欢迎留言告诉我。对于已有在网络上能够搜到质量较高译文的文章,我就不做重复工作了。本人精力有限,翻译质量达不到出版书籍的程度,可能有些人看不懂,不过我相信这总会帮助到一些人。有空的时候我会返回来对之前的文章进行润色。
对应官方文档,基础部分,Actions,Reducers,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
}
}
注意:
- 我们不改变
state
参数。我们通过Object.assign()
来创建副本。Object.assign(state, { visibilityFilter: action.filter })
也是错的,它修改了第一个参数。你一定要将空对象作为第一个参数。你还可以使用对象展开操作符来实现:{ ...state, ...newState }
。 - 在
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_TODO
和TOGGLE_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-helper、updeep这样的工具或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
}
}
有什么办法让它易于理解吗?看起来todos
或visibilityFilter
完全独立地被更新。有时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负责:
- 保留应用的状态
- 通过
getState()
来获取state - 通过
dispatch(action)
来更新状态 - 通过
subscribe(listener)
来注册监听器 - 通过
subscribe(listener)
返回的函数来处理未注册的监听器
注意在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)