Why use Redux?
React 只是 DOM 的一个抽象层,是 MVC 中的 V 层,并不是完整的Web应用解决方案,如果应用的交互比较多,那么只使用React,会使得代码复杂,不易阅读,Redux使用Flux架构的概念,与函数式编程结合,为React提供状态管理,适用于:多交互、多数据源的应用场景。
Redux简介
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。应用中所有的 state 都以一个对象树的形式储存在一个单一的 store 中。 惟一改变 state 的办法是触发 action,一个描述发生什么的对象。 为了描述 action 如何改变 state 树,你需要编写 reducers。
Redux的工作流程
特别说明:图片来源于Redux从设计到源码
相关核心概念
- Store: 保存数据的地方,你可以把它看成一个容器,整个应用只能有一个Store。
- State: Store对象包含所有数据,如果想得到某个时点的数据,就要对Store生成快照,这种时点的数据集合,就叫做State。
- Action: State的变化,会导致View的变化。但是,用户接触不到State,只能接触到View。所以,State的变化必须是View导致的。Action就是View发出的通知,表示State应该要发生变化了。
- Action Creator: View要发送多少种消息,就会有多少种Action。如果都手写,会很麻烦,所以我们定义一个函数来生成Action,这个函数就叫Action Creator。
- Reducer: Store收到Action以后,必须给出一个新的State,这样View才会发生变化。这种State的计算过程就叫做Reducer。Reducer是一个函数,它接受Action和当前State作为参数,返回一个新的State。
- dispatch: 是View发出Action的唯一方法。
redux整个工作流程
- 首先,用户(通过View)发出Action,发出方式就用到了dispatch方法;
- 然后,Store自动调用Reducer,并且传入两个参数:当前State和收到的Action,Reducer会返回新的State;
- State一旦有变化,Store就会调用监听函数,来更新View。
特别说明:可以看出,在reudx的整个工作流程中数据都是单向流动的,这种方式保证了流程的清晰。
Redux三大基础原则
- 单一数据源
整个应用的state被存储在一棵Object tree 中,他只有一个单一的store - state是只读的
唯一改变state的办法就是触发action,action是一个描述要发生什么的对象 - 纯函数的形式执行修改
为了描述action如何改变state tree ,你需要编写reducer,每一个reducer都是一个纯函数
redux基础
Action
State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过store.dispatch()
将 action
传到 store
。
Action的本质是一个普通的javascript对象,用来表示即将改变 state 的意图。我们约定:action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。 多数情况下,type 会被定义成字符串常量
。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
Action是将数据放入 store 的唯一途径。无论是从 UI 事件、网络回调,还是其他诸如 WebSocket 之类的数据源所获得的数据,最终都会被 dispatch 成 action。
约定俗成:action 必须拥有一个 type 域,它指明了需要被执行的 action type。Type 可以被定义为常量,然后从其他 module 导入。比起用 Symbols 表示 type,使用 String 是更好的方法,因为 string 可以被序列化。
特别注意: Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action,它会运送数据到 Store。
Action Creator
Action Creator 就是生成action的方法,view 要发送多种消息,不一定都要手写,定义一个函数来生成action,会方便很多。
例如:
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
reducer(用来更新state)
设计 State 结构
在 Redux 应用中,所有的 state
都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。
Action 处理
在确定了 state 对象的结构之后,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(previousState, action) => newState
reducer
的形式为 (state, action) => state
的纯函数。
描述了 action
如何把 旧的state
转变成新的 state
。state
的形式取决于你,可以是基本类型、数组、对象、甚至是 Immutable.js
生成的数据结构。惟一的要点是:当 state
变化时需要返回全新的对象,而不是修改传入的参数。
特别注意:需要谨记 reducer
一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
纯函数
纯函数是函数式编程的概念,必须遵守以下一些约束。
- 不得改写参数
- 不能调用系统 I/O 的API
- 不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果
我们将以指定 state 的初始状态(initialState)
作为开始。Redux 首次执行时,state 为 undefined,此时我们可借机设置并返回应用的初始 state。
import { VisibilityFilters } from './actions';
const initialState = {
visibilityFilter: VisibilityFilters.SHOW_ALL,
todos: []
};
function todoApp(state, action) {
if (typeof state === 'undefined') {
return initialState;
}
// 这里暂不处理任何 action,
// 仅返回传入的 state。
return state;
}
这里一个技巧是使用 ES6 参数默认值语法 来精简代码。
function todoApp(state = initialState, action) {
// 这里暂不处理任何 action,
// 仅返回传入的 state。
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 })
,因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用{ ...state, ...newState }
达到相同的目的。 - 在 default 情况下返回旧的 state
遇到未知的 action 时,一定要返回旧的 state。
处理多个 action
还有两个 action 需要处理。让我们先处理 ADD_TODO。
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 在末尾加上新建的 todo。而这个新的 todo 又是基于 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
})
})
我们需要修改数组中指定的数据项而又不希望导致突变, 因此我们的做法是在创建一个新的数组后, 将那些无需修改的项原封不动移入, 接着对需修改的项用新生成的对象替换。
Store
Store 就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个 Store。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供 getState() 方法获取 state;
- 提供 dispatch(action) 方法更新 state;
- 通过 subscribe(listener) 注册监听器;
- 通过 subscribe(listener) 返回的函数注销监听器。
特别注意:
Redux 应用只有一个单一的 store。
根据已有的 reducer 来创建 store 是非常容易的。一般使用 combineReducers()
将多个 reducer 合并成为一个。现在我们将其导入,并传递 createStore()
(用来生成store)。
import { createStore } from 'redux';
import todoApp from './reducers';
let store = createStore(todoApp);
createStore()
的第二个参数是可选的, 用于设置 state 初始状态。这对开发同构应用时非常有用,服务器端 redux 应用的 state 结构可以与客户端保持一致, 那么客户端可以将从网络接收到的服务端 state 直接用于本地数据初始化。
//创建store
let store = createStore(todoApp, window.STATE_FROM_SERVER)
State
Store对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
当前时刻的 State,可以通过store.getState()
拿到。
发起actions
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions';
//打印初始状态
console.log(store.getState());
//每次state更新时,打印日志
//注意subscribe()返回一个函数用来注销监听器
//unsubscribe是subscribe()返回的用来注销监听器的函数
let 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();
store.dispatch()
store.dispatch()是 View 发出 Action 的唯一方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
上面代码中,store.dispatch接受一个 Action 对象
作为参数,将它发送出去。
需要知道的是: store.dispatch方法会触发 Reducer 的自动执行,不需要我们手动去调用Reducer。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。
import { createStore } from 'redux';
const store = createStore(reducer);
上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。
store.subscribe()
Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listener,就会实现 View 的自动渲染。
store.subscribe方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
//解除监听
unsubscribe();
数据流
严格的单向数据流是 Redux 架构的设计核心。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
- 调用 store.dispatch(action)
Action 就是一个描述”发生了什么”的普通对象。比如:
{
type: 'ADD_TODO',
text: 'Read the Redux docs.'
};
注意: 可以在任何地方调用 store.dispatch(action)
,包括组件中、XHR 回调中、甚至定时器中。
2. Redux store 调用传入的 reducer 函数
Store
会把两个参数传入 reducer
: 当前的 state 树和 action。
例如:在应用中,根 reducer 可能接收这样的数据:
// 当前应用的 state(todos 列表和选中的过滤器)
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [
{
text: 'Read the docs.',
complete: false
}
]
}
// 将要执行的 action(添加一个 todo)
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
}
// render 返回处理后的应用状态
let nextState = todoApp(previousState, action);
注意:reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action
前发生。
3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树
根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()
辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。
combineReducers()
的使用方法:
假设你有两个 reducer:
一个是 todo 列表,另一个是当前选择的过滤器设置:
//todo 列表
function todos(state = [], action) {
// 省略处理逻辑...
return nextState;
}
//当前选择的过滤器设置
function visibleTodoFilter(state = 'SHOW_ALL', action) {
// 省略处理逻辑
return nextState;
}
let todoApp = combineReducers({
todos,
visibleTodoFilter
})
当触发 action 后,combineReducers 返回的 todoApp 会负责调用两个 reducer:
let nextTodos = todos(state.todos, action);
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action);
然后会把两个结果集合并成一个 state 树:
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
};
虽然combineReducers()
是一个很方便的辅助工具,你也可以选择不用,你可以自行实现自己的根 reducer!
4. Redux store 保存了根 reducer 返回的完整 state 树
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener)
的监听器都将被调用;监听器里可以调用 store.getState()
获得当前 state。
现在,可以应用新的 state 来更新 UI。如果你使用了 React Redux
这类的绑定库,这时就应该调用 component.setState(newState)
来更新。
combineReducers(reducers)
随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分。
combineReducers
辅助函数的作用是:把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer
函数,然后就可以对这个 reducer
调用 createStore
。
合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。 state 对象的结构由传入的多个 reducer 的 key 决定。
返回值
一个调用 reducers 对象
里所有 reducer 的 reducer,并且构造一个与 reducers 对象结构相同的 state 对象。
相关规则
每个传入 combineReducers 的 reducer 都需满足以下规则:
所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
永远不能返回 undefined。当过早 return 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers 会抛异常。
如果传入的 state 就是 undefined,一定要返回对应 reducer 的初始 state。根据上一条规则,初始 state 禁止使用 undefined。使用 ES6 的默认参数值语法来设置初始 state 很容易,但你也可以手动检查第一个参数是否为 undefined。
最终,state 对象的结构会是这样的:
{
reducer1: ...
reducer2: ...
}
通过为传入对象的 reducer 命名不同来控制 state key 的命名。例如,你可以调用combineReducers({ todos: myTodosReducer, counter: myCounterReducer })
将 state 结构变为{ todos, counter }
。
应用中不要创建多个 store!相反,使用 combineReducers
来把多个 reducer 创建成一个根 reducer。
在 Redux 中,只有一个 store,但是 combineReducers 让你拥有多个 reducer,同时保持各自负责逻辑块的独立性。
如果 state 是普通对象,永远不要修改它!比如,reducer 里不要使用 Object.assign(state, newData),应该使用 Object.assign({}, state, newData)。这样才不会覆盖旧的 state。也可以使用 Babel 阶段 1 中的 ES7 对象的 spread 操作 特性中的 return { …state, …newData }。
异步操作
Redux的基本流程是这样的:用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。
Action 发出以后,Reducer 立即算出 State,这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
怎样处理异步的情况呢?怎么才能 Reducer 在异步操作结束后自动执行呢?这就要用到新的工具:中间件(middleware)。
中间件(middleware)
为了理解中间件,让我们站在框架作者的角度思考问题:如果要添加功能,你会在哪个环节添加?
(1)Reducer:纯函数,只承担计算 State 的功能,不合适承担其他功能,也承担不了,因为理论上,纯函数不能进行读写操作。
(2)View:与 State 一一对应,可以看作 State 的视觉层,也不合适承担其他功能。
(3)Action:存放数据的对象,即消息的载体,只能被别人操作,自己不能进行任何操作。
想来想去,只有发送 Action 的这个步骤,即store.dispatch()方法,可以添加功能。举例来说,要添加日志功能,把 Action 和 State 打印出来,可以对store.dispatch进行如下改造。
let next = store.dispatch;
store.dispatch = function dispatchAndLog(action) {
console.log('dispatching', action);
next(action);
console.log('next state', store.getState());
}
上面代码中,对store.dispatch进行了重定义,在发送 Action 前后添加了打印功能。这就是中间件的雏形。
中间件就是一个函数,对store.dispatch方法进行了改造,在发出 Action 和执行 Reducer 这两步之间,添加了其他功能。
中间件的用法
applyMiddleware(…middlewares)
applyMiddleware
是 Redux 的原生方法,作用是:将所有中间件组成一个数组,依次执行。源码如下:
export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer);
var dispatch = store.dispatch;
var chain = [];
var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {...store, dispatch}
}
}
上面代码中,所有中间件被放进了一个数组chain,然后嵌套执行,最后执行store.dispatch。可以看到,中间件内部(middlewareAPI)可以拿到getState和dispatch这两个方法。
异步操作的基本思路
同步操作只要发出一种 Action 即可,异步操作的差别是它要发出三种 Action。
- 操作发起时的 Action
- 操作成功时的 Action
- 操作失败时的 Action
以向服务器取出数据为例,三种 Action 可以有两种不同的写法。
// 写法一:名称相同,参数不同
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
// 写法二:名称不同
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
除了 Action 种类不同,异步操作的 State 也要进行改造,反映不同的操作状态。下面是 State 的一个例子。
let state = {
// ...
isFetching: true,
didInvalidate: true,
lastUpdated: 'xxxxxxx'
};
上面代码中,State 的属性isFetching
表示是否在抓取数据。didInvalidate
表示数据是否过时,lastUpdated
表示上一次更新时间。
现在,整个异步操作的思路就很清楚了。
操作开始时,送出一个 Action,触发 State 更新为”正在操作”状态,View 重新渲染
操作结束后,再送出一个 Action,触发 State 更新为”操作结束”状态,View 再一次重新渲染
redux-thunk 中间件
异步操作的一种解决方案就是:写出一个返回函数的 Action Creator
,然后使用redux-thunk
中间件改造store.dispatch
。正常情况下store.dispatch
方法的参数只能是对象,不能是函数。但是改造之后,store.dispatch
方法的参数可以是函数。
相关API
compose(…functions)
用来从右到左来组合多个函数。
这是函数式编程中的方法,为了方便,被放到了 Redux 里。 当需要把多个 store 增强器
依次执行的时候,需要用到它。
bindActionCreators(actionCreators, dispatch)
把 action creators 转成拥有同名 keys 的对象,但使用 dispatch 把每个 action creator 包围起来,这样可以直接调用它们。
相关参考:
Redux 中文文档
redux middleware 详解
redux applyMiddleware 原理剖析
Redux 入门教程(一):基本用法
Redux 入门教程(二):中间件与异步操作
Redux 入门教程(三):React-Redux 的用法
浅入react-native使用redux
redux学习笔记
Redux系列x:源码解析
React 实践心得:react-redux 之 connect 方法详解
深入浅出 - Redux
关于Redux的一些总结(一):Action & 中间件 & 异步