Open lbwa opened 5 years ago
强制使用 action
来修改 state
是为了更加清晰的理解 state
的变化。
state
和 action
是通过 reducer
来联系起来的。
reducer
应该是一个 纯函数, 它接受 state
和 action
作为参数,并且该函数的返回值即是一个修改后的 state
(并非原 state
对象,每次值的修改都将返回一个新的对象,此处涉及函数式编程中的 immutable value
概念)。
纯函数:无可见副作用(如文件读取,
ajax
等);相同输入始终返回相同输出。
当在一个复杂庞大的 app
中书写一个 reducer
较为困难时,可将 root reducer
拆分至各个业务模块中书写,以用于管理 state
树的各个部分。
Redux
可被描述为三项基本原则:
truth
来源即整个应用的 state
是被存储在一个包含唯一 object
树的 store
中的。
单一 store
树便于探测和 Debug
应用的状态,并且便于在开发模式下持久化应用的 state
。并且可传统技术上难以实现 撤销/恢复
的操作。
console.log(store.getState())
/* Prints
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
*/
state
是只读的唯一修改 state
的方式是派发一个 action
,其中出发 action
的参数是一个描述了发生了什么的对象。
这是为了确保 视图
和 请求回调
均无法直接修改 state
。因为所有的 state
改变都是中心化的并且是按照严格的顺序一步一步执行的。没有细小的竞争条件需要注意。actions
作为一个朴素的 object
,它可以被打印,序列化,存储,或为了 debug
或测试来在稍后重放。
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
为了展示当前应用的 state
树是如何转变的,你必须书写 pure reducers
(纯函数始终返回一个新值,旧值将被作为记录存储,这样就可是实现 time travel
,起到记录 state
树变化的目的)。
Reducer
是一个可接受一个先前 state
和 action
的 纯函数,并且返回一个修改后的新的 state
对象。应始终记住返回一个新的 state
对象,而不是修改传入的 state
对象。你可以在你的 app
中创建单个 reducer
,随着你的 app
的增长,可将 reducer
分离以用于管理指定 state
树的部分 state
。因为 reducer
只是一个函数,那么你可以控制调用这些 reducer
的顺序,传递额外的数据,或者甚至可以为共有的 task
(如分页)创建可复用的 reducer
。
import { combineReducers, createStore } from 'redux'
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 'COMPLETE_TODO':
return state.map((todo, index) => {
return index === action.index
? Object.assign({}, todo, {
completed: true
})
: todo
})
default:
return state
}
}
const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)
Actions
是携带了从 app
发送到 store
的数据载荷。这些载荷是 store
的 唯一 信息来源。你可以使用 store.dispatch()
来发送 actions
(即载荷)。
const ADD_TODO = 'ADD_TODO'
// Actions
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Actions
是一个朴素的 JavaScript
对象。Actions
必须有 type
属性以用于指定将要执行的动作(action
)。通常应该将 type
定义为字符串常量。一旦你的 app
足够庞大,你可能会想将这些 types
移动到一个单独的模块。
import { ADD_TODO, REMOVE_TODO } from './actionTypes'
关于抽离
action type
的说明将
action type
定义到一个单独的文件中并不是必要的步骤,或者仅仅定义action type
中的一部分。对于规模较小的项目,可能直接使用字符串字面量来定义action type
更为简单。然而,这有一些在庞大的codebases
中显式地声明变量的好处。阅读 Reducing Boilerplate 来获得更多的实践性建议来保持你的codebase
简洁。
在一个 action
对象上除了 type
属性以外其他的属性,你都可以任意定义。如果你感兴趣,可阅读 [Flux Standard Action] 来获得如何组建 action
对象的推荐。
Action creators
是创建 action
的函数。很容易将 action
和 action creator
概念混为一谈,所以尽可能的使用正确的术语。
在 Redux
中,action creator
简单返回一个 action
:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
在传统的 Flux 架构中,action creators
被调用时经常会触发一个 dispatch
,如下:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text
}
dispatch(action)
}
而在 Redux
中并不是这样。相反地,传入结果到 dispatch()
函数中来实现初始化一个 dispatch
。
dispatch(addTodo(text))
dispatch(completeTodo(index))
可选地,你可以创建一个实现绑定的自动 dispatch
的 action creator
。
const boundAddTodo => dispatch(addTodo(text))
const boundCompleteTodo => dispatch(completeTodo(index))
现在你可以直接调用他们:
boundAddTodo(text)
boundCompleteTodo(index)
dispatch
函数在 store
中可直接通过 store.dispatch()
的形式访问,但是更多象 react-redux
中的 connect()
你可以直接调用这些帮手来访问 dispatch
。你可以使用 boundActionCreators()
来在一个 dispatch()
函数中绑定多个 action creators
。
action creator
可以是 异步 的,并可有 副作用。你可阅读 提高指南中的async actions来学习如何监听 AJAX
响应,并结合 action creators
到异步控制流中。
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
表明了应用的 state
如何响应 actions
的修改,并发送到 store
中。记住 actions
只描述了发生了什么,但是没有描述应用的 state
是如何发生改变的。
state
的结构在 Redux
中,所有的应用 state
都是被作为单个对象存储的。
在一个足够复杂的
App
中,你可能希望相互引用他们。我们建议你保持state
尽可能的没有嵌套。保证每一个数据都有一个ID
作为key
在一个对象中存储。并且可使用该ID
来从其他的数据中来引用该数据。这种方案在 normalizr's 文档中有更多的细节。例如,在state
中使用todosById: { id -> todo }
和todos:array<id>
是一个非常好的方案(译者:利于嵌套数据的查找)。
Reducer
是一个接收 action
和将要被修改的 state
,并且返回新的修改后的 state
。
;(previousState, action) => newState
之所以叫 reducer
是因为它是一种会当作 Array.prototype.reduce(reducer, ?initialValue)
的参数的函数。reducer
保持纯洁性是非常重要的。下面是你永远都 不应该 在一个 reducer
内部做的事情(即定义纯函数的最低要求):
reducer
的参数(对于任何修改,都应返回一个新值,而不是在原值上修改);API
调用或路由切换(应避免可见的函数副作用);Date.now()
或 Math.random()
(相同的输入始终应该返回相同的输出)。我们将在 [advanced walkthrough] 谈论如何执行函数副作用。而现在,只需要记住 reducer
必须 是纯洁的。 给定相同的阐述,reducer
因该计算下一个 state
并返回它。没有惊喜,没有副作用,没有 API
调用。没有值的修改(赋值),只有单纯的计算。
有了这些概念,让我们开始编写我们的 reducer
,并逐步理解之前定义的 actions
。
我们首先指定初始 state
。因为 Redux
在初始传入一个 undefined state
来调用我们的 reducer
,所以这是我们返回初始 state
的机会:
import { VisibilityFilters } from './actions'
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
}
function todoApp(state = initialState, action) {
switch (action.type) {
// handle `SET_VISIBILITY_FILTER`
case SET_VISIBILITY_FILTER:
// 为了保持函数纯洁性,始终通过 return 一个新值来达到修改原值的目的
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
// For now, don't handle any actions
// and just return the state given to us.
return state
}
值得注意的是:
我们没有修改 state
。我们用 Object.assign
创建了一个 state
的副本。调用 Object.assign(state, { visibilityFilter: action.filter })
是不正确的:因为会修改第一个参数,即 state
。你 必须 提供一个空的对象作为第一个参数。你同样可使用 扩展运算符
即 `{ ...state, ...newState } 来实现同样的目的。
我们默认返回之前的 state
。 对于任意未知的 action
返回先前传入的 state
对象是十分重要的。
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
})
})
因为我们想在不发生重排的情况下修改指定的项的值,我们就必须创建一个具有相同项的数组除了特定项。如果你发现你经常书写这样的操作,推荐使用像 immutability-helper,updeep 甚至像 Immutable 这样原生支持深度更新的库。只需要记住除非你首先克隆了 state
对象,否则永远不要在 state
对象内部做任何赋值操作。
在之前章节,我们 定义了 actions
用来表述 发生了什么
这一事实,并 定义了reducers
表述了根据这些 actions
做出的更新改变。
store
将是一个和 actions
、reducers
组合在一起的对象。store
有如下职责:
getState()
来访问 state
;dispatch(action)
来更新 state
;subscribe(listener)
注册监听器;subscribe(listener)
返回的函数注销监听器在一个 Redux
应用中,应该始终只有单个 store
。 当你想分离数据处理逻辑时,你应使用 reducers composition 而不是创建多个 store
。
如果你有一个 reducer
,那么创建一个 store
也是非常简单的。在之前的章节中,我们使用 combineReducers() 来合并多个 reducers
为一个。我们现在引入之前的 root reducers
,并将它传入 createStore() 中。
import { createStore } from 'redux'
import todoApp from './reducers'
const store = createStore(todoApp)
可选地,你可向 createStore()
传入第二个参数作为初始 state
。这对于客户端 state
保持与服务端 Redux
应用的 state
是非常有帮助的。
const store = createStore(todoApp, window.STATE_FROM_SERVER)
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters
} from './actions'
// 打印初始 state
console.log(store.getState())
// 每次 state 改变,都会打印新的 state
// 注意 subscribe() 返回的一个函数可用于注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()))
// 分发一些 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))
// 停止监听 state 的更新
unsubscribe()
一个基础的 Flux Standard Action:
{
type: 'ADD_TODO',
payload: {
text: 'Do something.'
}
}
一个展示了一个错误的的 Flux standard action, 类似于一个 rejected 的 Promise 实例。
{
type: 'ADD_TODO',
payload: new Error(),
error: true
}
一个 action
必须 是
JavaScript
对象;type
属性。一个 action
可能
error
属性;payload
属性;meta
属性。一个 action
必须不能包含 除 type
,payload
,error
,meta
以外的属性。
type
一个 action
的 type
向 consumer
标识了被触发的 action
的实质(意译,即是哪一类 action
的意思)。type
是一个字符串常量。如果两个 type
相同,那么他们 必须 严格相等(===
)。
payload
可选属性 payload
可以是任意类型值。它表示了 action
的载荷。任何关于 action
非 type
或 aciton
的状态的信息都应该是 payload
属性的组成部分。
按照惯例,若 error
是 true
值,payload
应该 是一个 error
对象。这类似与一个携带一个 error
对象的 rejected
状态的 Promise
实例。
error
若一个 aciton
表示一个 error
,那么可选属性 error
可以 被设置为 true
。
一个 包含 error
属性为 true
的 action
类似与一个 rejected
状态的 Promise
实例。按照惯例,payload
应该 是一个 error
对象。
若 error
属性有任何非 true
值,包含 undefined
和 null
,那么这个 action
都 不应该 被解读为一个 error
。
meta
The optional meta
property MAY be any type of value. It is intended for any extra information that is not part of the payload.
可选属性 meta
可以 是任意类型值。它表示任何不属于 payload
属性的额外信息。
The module flux-standard-action
is available on npm. It exports a few utility functions.
我们如何使用定义好的同步 action creators
来和网络请求一起工作?标准的做法是使用 Redux Thunk 中间件。该中间件有一个独立的 package
称为 redux-thunk
。我们稍后会解释中间件是如何工作的。现在,只有一件重要的事是需要读者知道的:通过使用指定的中间件,一个 action creator
可以返回一个函数而不是一个 action
对象。在这种方法中,action creator
成为了一个 thunk
Thunk 在计算机科学中,一个
thunk
是指一个用于一个子程序向另外一个子程序注入额外计算结果的子程序。Thunk
主要用于注入直至需要该结果的计算结果,或在其他子程序的开端或结尾插入操作。它同时可简单地被理解为一个不带参数的函数,直到该函数被调用。
当一个 action creator
返回一个函数,那么该函数将被 redux thunk
中间件执行。该函数不必是纯函数;因此该函数允许有包含执行异步 API
调用的副作用。该函数也可分发 action
—— 就像我们之前定义的同步的 actions
一样。
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
// 来见识我们第一个 thunk action creator!
// 尽管现在的函数内部不同于先前的 action creator,但你仍可以就像先前的 action creator
// 的调用方式来调用异步的 action creator:store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware knows how to handle functions.
// Thunk 中间件知道如何处理这些异步 action creator 函数。
// It passes the dispatch method as an argument to the function,
// 中间件会把 dispatch 方法作为本函数的一个参数传入,
// thus making it able to dispatch actions itself.
// 进而使得它自己能够分发对应的 action。
return function(dispatch) {
// First dispatch: the app state is updated to inform
// 第一个 dispatch; 更新 App 状态以通知 API 调用开始。
// that the API call is starting.
dispatch(requestPosts(subreddit))
// The function called by the thunk middleware can return a value,
// 通过 Thunk 中间件调用的函数可返回一个值,
// that is passed on as the return value of the dispatch method.
// 该值可作为 dispatch 方法的返回值传递。
// In this case, we return a promise to wait for.
// 在这种情况下,我们返回一个处于等待状态的 Promise 实例
// This is not required by thunk middleware, but it is convenient for us.
// 对于 thunk 中间件,返回 Promise 实例并不是必须的,但这可为我们提供很多的便利。
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// Do not use catch, because that will also catch
// 不要使用 catch 函数
// any errors in the dispatch and resulting render,
// 因为所有的错误会在 dispatch 和 结果渲染中被处理,
// causing a loop of 'Unexpected batch number' errors.
// 使用 catch 将导致一个 `Unexpected batch number` 的错误
// https://github.com/facebook/react/issues/6895
error => console.log('An error occurred.', error)
)
.then(json =>
// We can dispatch many times!
// 我们可以 dispatch 很多次!
// Here, we update the app state with the results of the API call.
// 在这,我们随着 API 调用的返回结果来更新 app 的 state
dispatch(receivePosts(subreddit, json))
)
}
}
而不是直接修改
state
,你可以指定任意修改object
的mutations
,我们叫这种mutations
叫做actions
。然后你写下一个叫做reducer
的纯函数用于决定每一个actions
是如何转换整个app
的state
。在一个典型的
Redux app
中,只有唯一一个store
和root reducing
函数(即root reducer
)。随着你的app
的增长,你可以将root reducer
拆分为分布在state
树不同部分的更小的reducer
。这就像在一个React app
中只存在一个根组件,但它是由许多更小的组件组合而成的。这个架构似乎看起来对于一个
counter
应用是有点杀鸡用牛刀(overkill
)了,但是这种模式的绝妙之处在于它可以很好的拓展至一个庞大并且复杂的app
。它也是非常强劲的开发者工具,因为可追踪每个引起mutation
的源头action
。你可以记录用户绘画并且通过重放每一个action
来重现用户会话。