简介
1、一句话概括
Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
2、action、reducer 和 store
所有的state都以一个对象树的形式,存储在一个单一的store中。唯一改变state的办法是触发action——1个描述发生什么的对象。为了描述action如何改变state,需要编写reducers。
import { createStore } from 'redux';
// reducer
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}
// 创建 Redux store 来存放应用的状态。
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);
// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
console.log(store.getState())
);
// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1
3、Redux的目的
现在,JavaScript 需要管理比任何时候都要多的 state (状态),而管理不断变化的 state 非常困难。
这些 state 可能包括服务器响应、缓存数据、本地生成尚未持久化到服务器的数据,也包括 UI 状态,如激活的路由,被选中的标签,是否显示加载动效或者分页器等等。
这里的复杂性很大程度上来自于:我们总是将两个难以理清的概念混淆在一起:变化和异步。 如果把二者分开,能做的很好,但混到一起,就变得一团糟。Redux就是为了帮你解决这个问题。
4、三大原则
单一数据源:整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。
State 是只读的:唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
使用纯函数来执行修改:为了描述 action 如何改变 state tree ,你需要编写 reducers。
多个reducer可以使用函数combineReducers合并:
import { combineReducers, createStore } from 'redux'
let reducer = combineReducers({ visibilityFilter, todos })
let store = createStore(reducer)
基础
Action
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch() 将 action 传到 store。
Action 创建函数
在 Redux 中的 action 创建函数只是简单的返回一个 action:
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
Redux 中只需把 action 创建函数的结果传给 dispatch() 方法即可发起一次 dispatch 过程。
dispatch(addTodo(text))
dispatch(completeTodo(index))
或者创建一个被绑定的 action 创建函数来自动 dispatch:
const boundAddTodo = (text) => dispatch(addTodo(text))
const boundCompleteTodo = (index) => dispatch(completeTodo(index))
store 里能直接通过 store.dispatch() 调用 dispatch() 方法,但是多数情况下你会使用 react-redux 提供的 connect() 帮助器来调用。bindActionCreators() 可以自动把多个 action 创建函数绑定到 dispatch() 方法上。
Reducer
Reducer描述了如何更新state。
1、设计State结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。建议在写代码前先想一下这个对象的结构。如何才能以最简的形式把应用的 state 用对象描述出来?
2、Action 处理
reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(previousState, action) => newState
保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
修改传入参数;
执行有副作用的操作,如 API 请求和路由跳转;
调用非纯函数,如 Date.now() 或 Math.random()。
Reducer重要原则:只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
另外注意:不要修改state;在default情况下返回旧的state。
3、处理多个action
4、拆分Reducer
Store
前面的章节讲述了:用 action 来描述“发生了什么”,用 reducers 来根据 action 更新 state 的用法。
Store的职责:
维持应用的 state;
提供 getState() 方法获取 state;
提供 dispatch(action) 方法更新 state;
通过 subscribe(listener) 注册监听器;
通过 subscribe(listener) 返回的函数注销监听器。
创建store:
import { createStore } from 'redux'
import todoApp from './reducers'
let store = createStore(todoApp)
//createStore() 的第二个参数是可选的, 用于设置 state 初始状态。
dispatch示例:
import { addTodo, toggleTodo, setVisibilityFilter, VisibilityFilters } from './actions'
// 打印初始状态
console.log(store.getState())
// 每次 state 更新时,打印日志
// 注意 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();
数据流
严格的单向数据流是 Redux 架构的设计核心。
这意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。
Redux 应用中数据的生命周期遵循下面 4 个步骤:
1、调用 store.dispatch(action)。
2、Redux store 调用传入的 reducer 函数。
注意 reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。
3、根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。
4、Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state!
所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。
现在,可以应用新的 state 来更新 UI。如果你使用了 React Redux 这类的绑定库,这时就应该调用 component.setState(newState) 来更新。
搭配React
这里需要再强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。
容器组件(Smart/Container Components)和展示组件(Dumb/Presentational Components)
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和 Redux store 连接起来。
展示组件
这些组件只定义外观并不关心数据来源和如何改变。传入什么就渲染什么。如果你把代码从 Redux 迁移到别的架构,这些组件可以不做任何改动直接使用。它们并不依赖于 Redux。
容器组件
还需要一些容器组件来把展示组件连接到 Redux。
技术上讲,容器组件就是使用 store.subscribe() 从 Redux state 树中读取部分数据,并通过 props 来把这些数据提供给要渲染的组件。
你可以手工来开发容器组件,但建议使用 React Redux 库的 connect() 方法来生成,这个方法做了性能优化来避免很多不必要的重复渲染。
重要的事情说三遍。
使用 connect() 前,需要先定义 mapStateToProps 这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。
最后,使用 connect() 创建新的组件 VisibleTodoList,并传入mapStateToProps和展示组件。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
传入Store
所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因为必须要用 store 把展示组件包裹一层,仅仅是因为恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件 Provider 来 魔法般的让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。
import React from 'react'
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import { createStore } from 'redux'
import todoApp from './reducers'
import App from './components/App'
let store = createStore(todoApp)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
高级
异步Action
当调用异步 API 时,有两个非常关键的时刻:发起请求的时刻,和接收到响应的时刻(也可能是超时)。
这两个时刻都可能会更改应用的 state;为此,你需要 dispatch 普通的同步 action。一般情况下,每个 API 请求都需要 dispatch 至少三种 action:
通知 reducer 请求开始的 action。
通知 reducer 请求成功的 action。
通知 reducer 请求失败的 action。
Redux Thunk中间件
通过使用指定的 middleware,action 创建函数除了返回 action 对象外还可以返回函数。这时,这个 action 创建函数就成为了 thunk。
而,重点来了,当 action 创建函数返回函数时,这个函数会被 Redux Thunk middleware 执行。这个函数并不需要保持纯净;它还可以带有副作用,包括执行异步 API 请求。这个函数还可以 dispatch action,就像 dispatch 前面定义的同步 action 一样。
action写法:
import fetch from 'isomorphic-fetch'
// 来看一下我们写的第一个 thunk action 创建函数!
// 虽然内部操作不同,你可以像其它 action 创建函数 一样使用它:
// store.dispatch(fetchPosts('reactjs'))
export function fetchPosts(subreddit) {
// Thunk middleware 知道如何处理函数。
// 这里把 dispatch 方法通过参数的形式传给函数,
// 以此来让它自己也能 dispatch action。
return function (dispatch) {
// 首次 dispatch:更新应用的 state 来通知
// API 请求发起了。
dispatch(requestPosts(subreddit))
// thunk middleware 调用的函数可以有返回值,
// 它会被当作 dispatch 方法的返回值传递。
// 这个案例中,
// 我们返回一个等待处理的 promise。
// 这并不是 redux middleware 所必须的,但这对于我们而言很方便。
return fetch(`http://www.subreddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json =>
// 可以多次 dispatch!
// 这里,使用 API 请求结果来更新应用的 state。
dispatch(receivePosts(subreddit, json))
)
// 在实际应用中,还需要
// 捕获网络请求的异常。
}
}
index中引入 Redux Thunk middleware 的方法:
import thunkMiddleware from 'redux-thunk'
import createLogger from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // 允许我们 dispatch() 函数
loggerMiddleware // 一个很便捷的 middleware,用来打印 action 日志
)
)
store.dispatch(selectSubreddit('reactjs'))
store.dispatch(fetchPosts('reactjs')).then(() =>
console.log(store.getState())
)
异步数据流 !Important
默认情况下,createStore() 所创建的 Redux store 没有使用 middleware,所以只支持同步数据流。你可以使用 applyMiddleware() 来增强 createStore()。
像 redux-thunk 或 redux-promise 这样支持异步的 middleware 都包装了 store 的 dispatch() 方法,以此来让你 dispatch 一些除了 action 以外的其他内容,例如:函数或者 Promise。你所使用的任何 middleware 都可以以自己的方式解析你 dispatch 的任何内容,并继续传递 actions 给下一个 middleware。比如,支持 Promise 的 middleware 能够拦截 Promise,然后为每个 Promise 异步地 dispatch 一对 begin/end actions。
当 middleware 链中的最后一个 middleware 开始 dispatch action 时,这个 action 必须是一个普通对象。这是同步式的 Redux 数据流开始的地方(译注:这里应该是指,你可以使用任意多异步的 middleware 去做你想做的事情,但是需要使用普通对象作为最后一个被 dispatch 的 action ,来将处理流程带回同步方式)。
Middleware
Middleware提供的是位于 action 被发起之后,到达 reducer 之前的扩展点。 你可以利用 Redux middleware 来进行日志记录、创建崩溃报告、调用异步接口或者路由等等。
搭配 React Router
先给出一个栗子:
const Root = () => (
<Router>
<Route path="/" component={App} />
</Router>
);
使用Provider包裹Router可以便于路由处理防伪store
const Root = ({ store }) => (
<Provider store={store}>
<Router>
<Route path="/" component={App} />
</Router>
</Provider>
);
bindActionCreators
栗子:
const mapStateToProps = ( state, props ) => {
const refund = state.get( 'refund' );
const refundList = refund.get( 'refundList' );
return {
list
}
};
const bindActionAndCreators = ( dispatch ) => ( {
action: bindActionCreators( action, dispatch )
} );
export default connect( mapStateToProps, bindActionAndCreators)( Form.create()( List ) )