Redux
1. 基本
工作流程:
工作流程图2:
1.1. redux 有三个原则
global 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
}
]
}
*/
使用一个对象来发出动作,是唯一更改状态的方法
store.dispatch({
type: 'COMPLETE_TODO',
index: 1
})
store.dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: 'SHOW_COMPLETED'
})
更改状态树需要编写 reducers
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) => {
if (index === action.index) {
return Object.assign({}, todo, {
completed: true
})
}
return todo
})
default:
return state
}
}
import { combineReducers, createStore } from 'redux'
const reducer = combineReducers({ visibilityFilter, todos })
const store = createStore(reducer)
1.2.核心概念
假设:一个应用程序是一个对象:
{
todos: [{
text: 'Eat food',
completed: true
}, {
text: 'Exercise',
completed: false
}],
visibilityFilter: 'SHOW_COMPLETED'
}
这样一来,代码的不同部分就无法任意更改状态,从而导致难以 重现 的错误。
要更改状态中的某些内容,您需要调度一个动作。 描述发生了什么。以下是一些示例操作:
{ type: 'ADD_TODO', text: 'Go to swimming pool' }
{ type: 'TOGGLE_TODO', index: 1 }
{ type: 'SET_VISIBILITY_FILTER', filter: 'SHOW_ALL' }
通过将每项更改都描述为一项操作,我们可以清楚地了解应用程序中正在发生的事情。如果有什么变化,我们知道为什么会变化。动作就像发生了什么的面包屑。
最后,为了将状态和动作联系在一起,我们编写了一个称为reducer的函数。
再说一次,没有什么神奇的了,它只是一个将状态和操作作为参数并返回应用程序的下一个状态的函数。对于大型应用程序很难编写这样的功能,因此我们编写了一些较小的功能来管理部分状态:
function visibilityFilter(state = 'SHOW_ALL', action) {
if (action.type === 'SET_VISIBILITY_FILTER') {
return action.filter
} else {
return state
}
}
function todos(state = [], action) {
switch (action.type) {
case 'ADD_TODO':
return state.concat([{ text: action.text, completed: false }])
case 'TOGGLE_TODO':
return state.map((todo, index) =>
action.index === index
? { text: todo.text, completed: !todo.completed }
: todo
)
default:
return state
}
}
然后,我们编写另一个reducer,通过调用这两个reducer来对应的状态键来管理应用的完整状态:
function todoApp(state = {}, action) {
return {
todos: todos(state.todos, action),
visibilityFilter: visibilityFilter(state.visibilityFilter, action)
}
}
这基本上是Redux的整体思想。请注意,我们尚未使用任何Redux API。它带有一些实用程序来简化这种模式,但是主要思想是您描述如何随着时间的推移响应操作对象来更新状态,并且您编写的代码90%只是纯JavaScript,而没有使用Redux本身、API或任何魔术方法。
1.3,Actions
添加一个新的待办事项:
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
动作是普通的JavaScript对象。动作必须具有type指示所执行动作类型的属性。类型通常应定义为字符串常量。一旦您的应用程序足够大,您可能需要将它们移动到单独的模块中。
import { ADD_TODO, REMOVE_TODO } from '../actionTypes'
我们将添加另一种操作类型,以描述用户完成待办事项的操作。 我们通过索引引用特定的待办事项,因为我们将它们存储在数组中。 在真实的应用程序中,每次创建新内容时都生成唯一的ID是比较明智的。
{
type: TOGGLE_TODO,
index: 5
}
最好在每个操作中传递尽可能少的数据。
Action Creators
在Redux中,动作创建者(Action Creators)只需返回一个动作:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
这使它们易于携带且易于测试
另外,您可以创建一个绑定动作创建器(Action Creators),该创建器自动调度:
const boundAddTodo = text => dispatch(addTodo(text))
const boundCompleteTodo = index => dispatch(completeTodo(index))
现在,您可以直接调用它们:
boundAddTodo(text)
boundCompleteTodo(index)
该dispatch()
功能可以直接从 store 的访问store.dispatch()
,但更可能你会使用一个助手来访问它像反应,终极版的connect()
。您可以bindActionCreators()
用来自动将许多动作创建者绑定到一个dispatch()
函数。
动作创建者也可以是异步的并且有副作用。您可以在高级教程中阅读有关异步操作的信息,以了解如何处理AJAX响应以及将操作创建者组成异步控制流。在完成基础教程之前,不要跳过异步操作,因为它涵盖了高级教程和异步操作的先决条件的其他重要概念。
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 }
}
1.4,Reducer
Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer
。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State。
const reducer = function (state, action) {
// ...
return new_state;
};
整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子。
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
const state = reducer(1, {
type: 'ADD',
payload: 2
});
上面代码中,reducer函数收到名为ADD的 Action 以后,就返回一个新的 State,作为加法的计算结果。其他运算的逻辑(比如减法),也可以根据 Action 的不同来实现。
实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。
import { createStore } from 'redux';
const store = createStore(reducer);
上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。
为什么这个函数叫做 Reducer 呢?因为它可以作为数组的reduce方法的参数。请看下面的例子,一系列 Action 对象按照顺序作为一个数组。
const actions = [
{ type: 'ADD', payload: 0 },
{ type: 'ADD', payload: 1 },
{ type: 'ADD', payload: 2 }
];
const total = actions.reduce(reducer, 0); // 3
上面代码中,数组actions表示依次有三个 Action,分别是加0、加1和加2。数组的reduce方法接受 Reducer 函数作为参数,就可以直接得到最终的状态3。
1.5,纯函数
Reducer 函数最重要的特征是,它是一个纯函数。也就是说,只要是同样的输入,必定得到同样的输出。
纯函数是函数式编程的概念,必须遵守以下一些约束。
不得改写参数
不能调用系统 I/O 的API
不能调用Date.now()或者Math.random()等不纯的方法,因为每次会得到不一样的结果
由于 Reducer 是纯函数,就可以保证同样的State,必定得到同样的 View。但也正因为这一点,Reducer 函数里面不能改变 State,必须返回一个全新的对象,请参考下面的写法。
// State 是一个对象
function reducer(state, action) {
return Object.assign({}, state, { thingToChange });
// 或者
return { ...state, ...newState };
}
// State 是一个数组
function reducer(state, action) {
return [...state, newItem];
}
最好把 State 对象设成只读。你没法改变它,要得到新的 State,唯一办法就是生成一个新对象。这样的好处是,任何时候,与某个 View 对应的 State 总是一个不变的对象。
1.6,store.subscribe() 函数
Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。
store.subscribe方法返回一个函数,调用这个函数就可以解除监听。
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
unsubscribe();
1.7,State 和 Store 的实现
Store对象包含所有数据。如果想得到某个时点的数据,就要对 Store 生成快照。这种时点的数据集合,就叫做 State。
当前时刻的 State,可以通过store.getState()拿到。
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
上一节介绍了 Redux 涉及的基本概念,可以发现 Store 提供了三个方法。
- store.getState()
- store.dispatch()
- store.subscribe()
import { createStore } from 'redux';
let { subscribe, dispatch, getState } = createStore(reducer)
createStore方法还可以接受第二个参数,表示 State 的最初状态。这通常是服务器给出的。
let store = createStore(todoApp, window.STATE_FROM_SERVER)
上面代码中,window.STATE_FROM_SERVER就是整个应用的状态初始值。注意,如果提供了这个参数,它会覆盖 Reducer 函数的默认初始值。
下面是createStore方法的一个简单实现,可以了解一下 Store 是怎么生成的。
const createStore = (reducer) => {
let state;
let listeners = [];
const getState = () => state;
const dispatch = (action) => {
state = reducer(state, action);
listeners.forEach(listener => listener());
};
const subscribe = (listener) => {
listeners.push(listener);
return () => {
listeners = listeners.filter(l => l !== listener);
}
};
dispatch({});
return { getState, dispatch, subscribe };
1.8,Reducer 的拆分
Reducer 函数负责生成 State。由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大。
请看下面的例子。
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
});
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
});
default: return state;
}
};
上面代码中,三种 Action 分别改变 State 的三个属性。
- ADD_CHAT:chatLog属性
- CHANGE_STATUS:statusMessage属性
- CHANGE_USERNAME:userName属性
这三个属性之间没有联系,这提示我们可以把 Reducer 函数拆分。不同的函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可。
const chatReducer = (state = defaultState, action = {}) => {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
};
上面代码中,Reducer 函数被拆成了三个小函数,每一个负责生成对应的属性。
这样一拆,Reducer 就易读易写多了。而且,这种拆分与 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 Reducer 完全可以对应。
Redux 提供了一个combineReducers方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer。
import { combineReducers } from 'redux';
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
export default todoApp;
上面的代码通过combineReducers方法将三个子 Reducer 合并成一个大的函数。
这种写法有一个前提,就是 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)
}
}
总之,combineReducers()做的就是产生一个整体的 Reducer 函数。该函数根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象。
下面是combineReducer的简单实现。
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
},
{}
);
};
};
你可以把所有子 Reducer 放在一个文件里面,然后统一引入。
import { combineReducers } from 'redux'
import * as reducers from './reducers'
const reducer = combineReducers(reducers)
2,中间件与异步操作
用户发出 Action,Reducer 函数算出新的 State,View 重新渲染。这叫做同步;Action 发出以后,过一段时间再执行 Reducer,这就是异步。
Reducer 在异步操作要用到新的工具:中间件(middleware)。
2.1,中间件的基本用法:
import { applyMiddleware, createStore } from 'redux';
import createLogger from 'redux-logger';
const logger = createLogger();
// applyMiddleware(...middleware) 中间件做为参数传入,一般logger中间件放置最后 ( applyMiddleware(thunk, promise, logger))
const store = createStore(
reducer,
applyMiddleware(logger)
);
上面代码中,redux-logger提供一个生成器createLogger
,可以生成日志中间件logger
。然后,将它放在applyMiddleware
方法之中,传入createStore
方法,就完成了store.dispatch()
的功能增强。
2.2,异步操作
理解了中间件以后,就可以处理异步操作了。
同步操作只要发出一种 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 再一次重新渲染
2.3,redux-thunk 中间件
Action 是由store.dispatch方法发送的。而store.dispatch方法正常情况下,参数只能是对象,不能是函数。
这时,就要使用中间件redux-thunk 。如下:
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import reducer from './reducers';
// Note: this API requires redux@>=3.1.0
const store = createStore(
reducer,
applyMiddleware(thunk)
);
2.4,redux-promise 中间件
另一种异步操作的解决方案,就是让 Action Creator 返回一个 Promise 对象。
这就需要使用redux-promise中间件。
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise';
import reducer from './reducers';
const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
);
//方法一
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
import { createAction } from 'redux-actions';
//方法二
class AsyncApp extends Component {
componentDidMount() {
const { dispatch, selectedPost } = this.props
// 发出同步 Action
dispatch(requestPosts(selectedPost));
// 发出异步 Action
dispatch(createAction(
'FETCH_POSTS',
fetch(`/some/API/${postTitle}.json`)
.then(response => response.json())
));
}
3,React-Redux的用法
是直接使用Redux,还是使用React-Redux。尽管虽然提供了便利,但是需要掌握额外的API,并且要遵守它的组件分解规范。
一,UI组件-呈现组件 - UI的呈现
- 只负责UI的呈现,不带有任何业务逻辑
- 没有状态(即不使用this.state这个变量)
- 所有数据都由参数(this.props)提供
- 不使用任何Redux的API
UI组件又称为“纯组件”,即它纯函数一样,纯粹由参数决定它的值。
const Title =
value => <h1>{value}</h1>;
二,容器组件 - 数据和逻辑
- 负责管理数据和业务逻辑,不负责UI的呈现
- 带有内部状态
- 使用Redux的API
React-Redux规定,所有的UI组件都由用户提供,容器组件则是由React-Redux自动生成。
三,connect
React-Redux提供connect方法,用于从UI组件生成容器组件。connect的意思,就是将这两个组件连起来。
import { connect } from 'react-redux'
// TodoList是UI组件,
// const Counter = ...
// VisibleTodoList 就是由React-Redux通过 connect 方法自动生成的 ---容器组件--- 。
const VisibleTodoList = connect()(TodoList);
-
(1)输入逻辑:外部的数据(即state对象)如何转换为UI组件的参数
-
(2)输出逻辑:用户发出的动作如何转换Action对象,从UI组件传出去。
因此,connect方法的完整API如下。
import { connect } from 'react-redux'
// const Counter = ...
const VisibleTodoList = connect(
mapStateToProps, //负责输入逻辑,即将state映射到UI组件的参数(props)
mapDispatchToProps //负责输出逻辑,即将用户对UI组件的操作 映射至动作。
)(TodoList)
四, mapStateToProps
mapStateToProps是一个函数。它的作用就是像它的名字那样,建立一个从(外部的)state对象到(UI组件的)props对象的映射关系。
作为函数,mapStateToProps执行后应该返回一个对象,里面的每一个键值对就是一个映射。请看下面的例子。
const mapStateToProps = (state) => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
mapStateToProps是一个函数,它接受state作为参数,返回一个对象。这个对象有一个todos属性,代表UI组件的同名参数,后面的也是getVisibleTodos一个函数,可以从state算出todos的值。
下面就是getVisibleTodos的一个例子,用来算出todos。
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_ALL':
return todos
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
default:
throw new Error('Unknown filter: ' + filter)
}
}
mapStateToProps会订阅Store,每当state更新的时候,就会自动执行,重新计算UI组件的参数,从而触发UI组件的重新渲染。
mapStateToProps的第一个参数总是state对象,还可以使用第二个参数,代表容器组件的props对象。
// 容器组件的代码
// <FilterLink filter="SHOW_ALL">
// All
// </FilterLink>
const mapStateToProps = (state, ownProps) => {
return {
active: ownProps.filter === state.visibilityFilter
}
}
使用ownProps作为参数后,如果容器组件的参数发生变化,也会引发UI组件重新渲染。
connect方法可以省略mapStateToProps参数,那样的话,UI组件就不会订阅Store,就是说Store的更新不会引起UI组件的更新。
五,mapDispatchToProps
mapDispatchToProps是connect函数的第二个参数,用于建立UI组件的参数到store.dispatch方法的映射。然后,它定义了该用户的操作应该认为Action,传给Store。它可以是一个函数,也可以是一个对象。
如果mapDispatchToProps是一个函数,会得到dispatch和ownProps(容器组件的props对象)两个参数。
// mapDispatchToProps 作为函数,应该返回一个对象,该对象的每个键值对都是一个映射,定义了UI组件的参数怎样发出。
const mapDispatchToProps = (
dispatch,
ownProps
) => {
return {
onClick: () => {
dispatch({
type: 'SET_VISIBILITY_FILTER',
filter: ownProps.filter
});
}
};
}
如果mapDispatchToProps的英文一个对象,它的每个键名也是对应UI组件的同名参数,键值应该是一个函数,会被当作行动的创建者,返回的动作会由终极版自动发出。举例来说,的上面mapDispatchToProps写成对象就是下面这样。
const mapDispatchToProps = {
onClick: (filter) => {
type: 'SET_VISIBILITY_FILTER',
filter: filter
};
}
六, Provider 组件
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成UI组件的参数。
一种解决方法是将state对象作为参数,放入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级 将 state 传下去下去很麻烦。
React-Redux提供Provider组件,可以让容器组件拿到state。
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp);
// Provider在根组件外面包了一层,这样一来,App的所有子组件就好像都可以拿到state了。
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
它的原理是React组件的 context 属性,请看原始码。
class Provider extends Component {
getChildContext() {
return {
store: this.props.store
};
}
render() {
return this.props.children;
}
}
Provider.childContextTypes = {
store: React.PropTypes.object
}
上面代码中,store 放在了上下文对象context上面。然后,子组件就可以从context拿到store,代码大致如下。
class VisibleTodoList extends Component {
componentDidMount() {
const { store } = this.context;
this.unsubscribe = store.subscribe(() =>
this.forceUpdate()
);
}
render() {
const props = this.props;
const { store } = this.context;
const state = store.getState();
// ...
}
}
VisibleTodoList.contextTypes = {
store: React.PropTypes.object
}
React-Redux自动生成的容器组件的代码,就类似上面这样,从而拿到store。
七, React-Router路由库
使用React-Router的项目,与其他项目没有不同之处,使用也是Provider在Router外面包一层,毕竟Provider的唯一功能就是传入store对象。
const Root = ({ store }) => (
<Provider store={store}>
<Router>
<Route path="/" component={App} />
</Router>
</Provider>
);