核心组件
核心组件
Redux 的整个系统图解可以参考下图:
-
Store - an object that keeps whole the state of our application
-
Actions - plain objects representing the facts about “what happened” in our app
-
Reducers - pure functions updating the state according to actions
All this staff responds to the side of logic. Let’s look further… What’s about view? There are two types of components:
-
Container - “smart” components,which are concerned with “how things work”
-
Presentational -“dumb” components,which are concerned with “how things look”
Action
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。多数情况下,type 会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
Action Creator
Action 创建函数 就是生成 action 的方法,在 Redux 中的 action 创建函数只是简单的返回一个 action:
function addTodo(text) {
return {
type: ADD_TODO,
text,
};
}
这样做将使 action 创建函数更容易被移植和测试。在 传统的 Flux 实现中,当调用 action 创建函数时,一般会触发一个 dispatch,像这样:
function addTodoWithDispatch(text) {
const action = {
type: ADD_TODO,
text,
};
dispatch(action);
}
不同的是,Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。
dispatch(addTodo(text));
dispatch(completeTodo(index));
或者创建一个被绑定的 action 创建函数来自动 dispatch:
const boundAddTodo = (text) => dispatch(addTodo(text));
const boundCompleteTodo = (index) => dispatch(completeTodo(index));
然后直接调用它们:
boundAddTodo(text);
boundCompleteTodo(index);
store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数 绑定到 dispatch() 方法上。
Reducer
Reducers 指定了应用状态的变化如何响应 actions 并发送到 store 的,记住 actions 只是描述了有事情发生了这一事实,并没有描述应用如何更新 state。
设计 State 的结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?以 todo 应用为例,需要保存两种不同的数据:
- 当前选中的任务过滤条件;
- 完整的任务列表。
通常,这个 state 树还需要存放其它一些数据,以及一些 UI 相关的 state。这样做没问题,但尽量把这些数据与 UI 相关的 state 分开。
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述。例如,实际开发中,在 state 里同时存放 todosById: { id -> todo }
和 todos: array<id>
是比较好的方式,本文中为了保持示例简单没有这样处理。
Action 处理
reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(previousState, action) => newState;
之所以将这样的函数称之为 reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue) 里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如 Date.now()或 Math.random()。
只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
import {
ADD_TODO,
TOGGLE_TODO,
SET_VISIBILITY_FILTER,
VisibilityFilters,
} from "./actions";
// ...
function todoReducer(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 在末尾加上新建的 todo。而这个新的 todo 又是基于 action 中的数据创建的。
combineReducer
combineReducers() 为我们提供了便捷的 Reducer 合并,所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 根据它们的 key 来筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。
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),
};
}
具体在 TodoApp 中,其用法如下:
import { combineReducers } from "redux";
function todosReducer(state = [], action) {
// ...
}
function visibilityFilterReducer(state = SHOW_ALL, action) {
// ...
}
const todoReducer = combineReducers({
visibilityFilter: visibilityFilterReducer,
todos: todosReducer,
});
Store
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
当需要拆分数据处理逻辑时,你应该使用 reducer 组合 而不是创建多个 store。在前一个章节中,我们使用 combineReducers() 将多个 reducer 合并成为一个。现在我们将其导入,并传递 createStore()。
import { createStore } from "redux";
import todoApp from "./reducers";
let store = createStore(todoApp);
createStore() 的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
let store = createStore(todoApp, window.STATE_FROM_SERVER);
发起 Actions
import {
addTodo,
toggleTodo,
setVisibilityFilter,
VisibilityFilters,
} from "./actions";
// 打印初始状态
console.log(store.getState());
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() => console.log(store.getState()));
// 发起一系列 action
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();