学习必备要点:
-
@H_403_3@首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理
@H_403_3@弄清楚Redux是如何实现状态管理的——store、action、reducer三个概念
@H_403_3@在React中集成Redux:redux + react-redux(多了一个概念——selector)
@H_403_3@Redux调试工具:redux devtools
redux结构图
其中红色虚线部分为redux的内部集成,不能显示的看到。
-
@H_403_3@action:是事件,它本质上是JavaScript的普通对象,它描述的是“发生了什么”。action由type:string和其他构成。
@H_403_3@reducer是一个监听器,只有它可以改变状态。是一个纯函数,它不能修改state,所以必须是生成一个新的state。在default情况下,必须但会旧的state。
@H_403_3@store是一个类似数据库的存储(或者可以叫做状态树),需要设计自己的数据结构来在状态树中存储自己的数据。
Redux入门
Redux简介
Redux是一个状态集中管理库。
安装
npm install --save redux
附加包
npm install --save react-redux
npm install --save-dev redux-devtools
三大原则
单一数据源
整个应用的state被存储在一棵object tree中,并且这个object tree只存在于唯一一个store中。
State是只读的
惟一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。
使用纯函数来执行修改
为了描述action如何改变状态树,我们需要编写reducers。Reducer只是一些纯函数,他接受先前的state和action,并返回新的state对象。
上图是Redux如何实现状态管理的框架,View(视图) 可以通过store.dispatch()方法传递action。 Action相当于事件模型中的事件,它描述发生了什么。Reducer相当于事件模型中的监听器,它接收一个旧的状态和一个action,从而处理state的更新逻辑,返回一个新的状态,存储到Store中。而从store–>view 的部分,则是通过mapStateToProps
这个函数来从Store中读取状态,然后通过props属性的方式注入到展示组件中。图中红色虚线部分是Redux内部处理,我们不必过多考虑这部分的实现。
Action
Action 是把数据从应用传到store的有效载荷,它是store数据的唯一来源,一般来说,我们通过store.dispatch()将action传到store。
Action创建函数
Action 创建函数 就是生成 action 的方法。“action” 和 “action 创建函数” 这两个概念很容易混在一起,使用时最好注意区分。
Redux中action创建函数只是简单返回一个action。
改变userName的示例:
export function changeUserName(userName) { // action创建函数
return { // 返回一个action
type: 'CHANGE_USERNAME',payload: userName,};
}
Action 本质上是JavaScript 普通对象。我们规定,action 内必须使用一个字符串类型的 type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
除了 type
字段外,action 对象的结构完全由你自己决定。参照 Flux 标准 Action 获取关于如何构造 action 的建议,另外还需要注意的是,我们应该尽量减少在action中传递数据。
Reducer
Action只是描述有事情发生这一事实,而Reducer用来描述应用是如何更新state。
设计State结构
在 Redux 应用中,所有的 state 都被保存在一个单一对象中。在写代码之前我们首先要想清楚这个对象的结构,要用最简单的形式把应用中的state用对象描述出来。
HelloApp应用的state结构很简单,只需要保存userName即可:
{userName: 'World'}
处理 Reducer 关系时的注意事项
开发复杂的应用时,不可避免会有一些数据相互引用。建议你尽可能地把 state 范式化,不存在嵌套。把所有数据放到一个对象里,每个数据以 ID 为主键,不同实体或列表间通过 ID 相互引用数据。把应用的 state 想像成数据库。这种方法在 normalizr 文档里有详细阐述
Action处理
确定了 state 对象的结构,就可以开始开发 reducer。reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(state,action) => newState
之所以称作 reducer 是因为它将被传递给 Array.prototype.reduce(reducer,?initialValue)
方法。保持 reducer 纯净非常重要。永远不要在 reducer 里做以下操作:
在后续的学习终将会介绍如何执行有副作用的操作,现在只需谨记reducer一定要保持纯净。只要传入参数相同,返回计算得到的下一个 state 就一定相同。没有特殊情况、没有副作用,没有 API 请求、没有变量修改,单纯执行计算。
我们将写一个reducer,让它来处理之前定义过的action。我们可以首先指定state的初始状态。
const initState = { /** 指定初始状态 */
userName: 'World!'
}
export default function helloAppReducer(state=initState,action) {
switch(action.type) {
case 'CHANGE_USERNAME':
return {
userName: action.payload,// 改变状态
};
default:
return state; // 返回旧状态
}
}
警告:
-
@H_403_3@
不要修改state
。如果涉及多个状态时,可以采用对象展开运算符的支持,来返回一个新的状态。 假设我们的实例中还存在其它状态,但是我们只需要改变userName
的值,那么上述示例我们可以采用以下方式返回新的状态:
return {
...state,userName: action.payload
}
@H_403_3@在default情况下返回旧的state
。 遇到未知的action时,一定要返回旧的state
。
Reducer拆分
这里我们以redux中文文档 中的todo应用为例来说明,在应用的需求中,有添加todo项,设置todo列表的过滤条件等多个action,同理我们就需要写多个reducer来描述状态是怎么改变的,建议把todo列表的更新和设置过滤条件放在两个reducer中去实现:
function todos(state = [],action) {
switch (action.type) {
case ADD_TODO:
return [
...state,{
text: action.text,completed: false
}
]
case TOGGLE_TODO:
return state.map((todo,index) => {
if (index === action.index) {
return {
...todo,completed: !todo.completed
}
}
return todo
})
default:
return state
}
}
function todoApp(state = initialState,action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return {
...state,visibilityFilter: action.filter
}
case ADD_TODO:
case TOGGLE_TODO:
return {
...state,todos: todos(state.todos,action)
}
default:
return state
}
}
todos
依旧接收 state
,但它变成了一个数组!现在 todoApp
只把需要更新的一部分 state 传给 todos
函数,todos
函数自己确定如何更新这部分数据。这就是所谓的 reducer 合成,它是开发 Redux 应用最基础的模式。
现在我们可以开发一个函数来做为主 reducer,它调用多个子 reducer 分别处理 state 中的一部分数据,然后再把这些数据合成一个大的单一对象。主 reducer 并不需要设置初始化时完整的 state。初始时,如果传入 undefined
,子 reducer 将负责返回它们的默认值。这个过程就是reducer合并。
下面的这段代码是reducer合并的两种方式:
export default function todoApp(state = {},action) {
return {
visibilityFilter: visibilityFilter(state.visibilityFilter,action),action)
}
}
每个 reducer 只负责管理全局 state 中它负责的一部分。每个 reducer 的 state 参数都不同,分别对应它管理的那部分 state 数据.
import { combineReducers } from 'redux';
const todoApp = combineReducers({
visibilityFilter,todos
})
export default todoApp;
combineReducers()
所做的只是生成一个函数,这个函数来调用你的一系列 reducer,每个 reducer 筛选出 state 中的一部分数据并处理,然后这个生成的函数再将所有 reducer 的结果合并成一个大的对象。
Store
前面的部分,我们学会使用action来描述发生了什么,使用reducers来根据action更新state的用法。
Store则是把action和reducers联系到一起的对象,它有以下职责:
-
@H_403_3@维持应用的 state;
@H_403_3@提供
getState()
方法获取 state;
@H_403_3@提供 dispatch(action)
方法更新 state;
@H_403_3@通过 subscribe(listener)
注册监听器;
@H_403_3@通过 subscribe(listener)
返回的函数注销监听器。
再次说明Redux应用只有一个单一的store。 当需要拆分处理数据逻辑时,我们应该使用 reducer 组合 而不是创建多个 store。
根据已有的reducer来创建store是非常容易的。在我们的HelloApp应用中,我们将helloAppReducer
导入,并传递给createStore()
。
import { createStore } from 'redux'
import helloAppReducer from './reducers'
let store = createStore(helloAppReducer) // 创建store
createStore()
的第二个参数是可选的,用于设置 state 初始状态。
备注:
其实这种数据结构是有reducer确定的,就像helloAPP的例子中,
const reducer = combineReducers({
hello: hello,city: cityReducer
})
而由redux-devtools
工具查看到的是下图这样的:
so,存储在store中的数据结构是由reducer确定的。
数据流
严格的单向数据流 是Redux架构的核心设计。这就意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。
Redux应用中数据的生命周期遵循以下4个步骤:
-
@H_403_3@
调用store.dispatch(action)
。
Action 就是一个描述“发生了什么”的普通对象。比如:
{ type: 'CHANGE_USERNAME',payload: "Welcome to Redux" };
我们可以在任何地方调用store.dispatch(action)
包括组件中、XHR回调中、甚至是定时器中。
Store 会把两个参数传入 reducer: 当前的 state 树和 action。
const initState = { /** 指定初始状态 */
userName: 'World!'
}
export default function helloAppReducer(state=initState,action) { // 传入两个参数
switch(action.type) {
case 'CHANGE_USERNAME':
return {
userName: action.payload,// 改变状态
};
default:
return state; // 返回当前状态
}
}
reducer 是纯函数。它仅仅用于计算下一个 state。它应该是完全可预测的:多次传入相同的输入必须产生相同的输出。它不应做有副作用的操作,如 API 调用或路由跳转。这些应该在 dispatch action 前发生。
@H_403_3@根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
根 reducer 的结构完全由我们自己决定。Redux 原生提供combineReducers()
辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。
Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个state。所有订阅store.subscribe(listener)
的监听器都将被调用;监听器里可以调用store.getState()
获取当前的state。
示例: Hello App
如果想查看示例的源码,请查看这里。Hello App源码
开始之前我们需要清楚实际上Redux和React之间并没有关系。Redux支持React、Angular、Ember、jQuery甚至纯JavaScript。即便如此,Redux 还是和 React 和 Deku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。
下面我们将用React来开发一个Hello World的简单应用。
安装React Redux
Redux默认并不包含 React 绑定库,需要单独安装。
npm install --save react-redux
容器组件和展示组件
Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。而容器组件和展示组件大致有以下不同:
展示组件 | 容器组件 |
---|---|
作用 | 描述如何展现内容、样式 |
是否能直接使用Redux | 否 |
数据来源 | props(属性) |
数据修改 | 从props中调用回调函数 |
调用方式 | 手动 |
大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和Redux store连接起来。
技术上来说我们可以直接使用 store.subscribe()
来编写容器组件。但不建议这么做,因为这样写就无法使用 React Redux 带来的性能优化。同样,不要手写容器组件,我们直接使用 React Redux 的 connect()
方法来生成,后面会详细介绍。
需求分析
我们的需求很简单,我们只是想要展示hello + userName,默认为“Hello World!”,当我们在输入框中输入不同的值时,会显示不同的“hello,_”问候语,由此可以分析出该应用只有一个状态,那就是{ userName: ‘张三’}
展示组件
该应用只有一个展示组件HelloPanel:
该组件之定义外观并不设计数据从哪里来,如果改变它,传入什么就渲染什么,如果你把代码从Redux迁移到别的架构,该组件可以不做任何改动直接使用。
容器组件
还需要一个容器组件来把展示组件连接到Redux。例如HelloPanel
组件需要一个状态类似HelloApp的容器来监听Redux store变化并处理如何过滤出要展示的数据。
HelloApp
根据当前显示状态来对展示组件进行渲染。
组件编码
export function changeUserName(userName) {
return {
type: 'CHANGE_USERNAME',};
}
@H_403_3@Reducer
index.js
const initState = { /** 指定初始状态 */
userName: 'World!'
}
export default function helloAppReducer(state=initState,action) {
switch(action.type) {
case 'CHANGE_USERNAME':
return {
userName: action.payload,// 改变状态
};
default:
return state; // 返回当前状态
}
}
@H_403_3@展示组件
HelloPanel.js
import React from 'react';
export default function HelloPanel(props) {
let input
return (
<div> <p>Hello,{props.userName}</p> <input ref={node => { input = node }} onChange={()=>props.onChange(input.value)}/> </div> ); }
@H_403_3@容器组件
使用 connect()
创建容器组件前,需要先定义 mapStateToProps
这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。例如:HelloApp
中需要计算
const mapStateToProps = (state) => {
return { userName: state.userName } // 返回期望注入到展示组件的props中的参数
};
除了读取state,容器组件还能分发action。类似的方式,可以定义 mapDispatchToProps()
方法接收 dispatch()
方法并返回期望注入到展示组件的 props 中的回调方法。
const mapDispatchToProps = (dispatch) => ({
onChange: (userName) => {
dispatch(changeUserName(userName)) // 返回期望注入到展示组件的 props 中的回调方法
}
})
最后,使用 connect()
创建 HelloApp
,并传入这两个函数。
import { connect } from 'react-redux';
import HelloPanel from './HelloPanel';
const HelloApp = connect( // 产生一个新的组件
mapStateToProps,mapDispatchToProps,)(HelloPanel)
这就是 React Redux API 的基础,但还漏了一些快捷技巧和强大的配置。建议仔细学习 React Redux文档。如果你担心 mapStateToProps
创建新对象太过频繁,可以学习如何使用 reselect 来 计算衍生数据。
传入Store
所有容器组件都可以访问 Redux store,所以可以手动监听它。一种方式是把它以 props 的形式传入到所有容器组件中。但这太麻烦了,因此必须要用 store
把展示组件包裹一层,恰好在组件树中渲染了一个容器组件。
建议的方式是使用指定的 React Redux 组件 <Provider>
来让所有容器组件都可以访问 store,而不必显示地传递它。只需要在渲染根组件时使用即可。
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import HelloApp from './HelloApp'
import HelloPanel from './reducers'
let store = createStore(HelloPanel)
render(
<Provider store={store}> <HelloApp /> </Provider>,document.getElementById('root') )
到这里,我们已经基本掌握了Redux的基础及核心概念,有了这些,我们就可以开发简单的应用,关于Redux的更多实例、高级应用、技巧、API文档等可以查看redux中文文档 。
子状态树与combineReducers(reducers)
简介
随着应用变得复杂,需要对 reducer 函数 进行拆分,拆分后的每一块独立负责管理 state 的一部分。
combineReducers
辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore
。
合并后的 reducer 可以调用各个子 reducer,并把它们的结果合并成一个 state 对象。state 对象的结构由传入的多个 reducer 的 key 决定。
最终,state 对象的结构会是这样的:
{ reducer1: ... reducer2: ... }
使用:
combineReducers({ hello,cityReducer })
state 对象的结构:
// 实际例子
{
"hello":{"userName":"张三"},"cityReducer":{"city":"北京"} }
通过为传入对象的 reducer 命名不同来控制 state key 的命名。
e.g.:
你可以调用 combineReducers({hello: hello,city: cityReducer})
将 state 结构变为{ hello,city }
通常的做法是命名 reducer,然后 state 再去分割那些信息,因此你可以使用 ES6 的简写方法:combineReducers({ hello,city })
。这与 combineReducers({ hello: hello,city: cityReducer })
一样。
对于reducer的结构,我们规定只能是一级的,也就是
{
"hello":{"userName":"张三"},"cityReducer":{"city":"北京"} }
这种结构,不能再有子树,这样是为了方便进行管理。
参数
reducers
(Object)是一个对象,它的值(value) 对应不同的 reducer 函数,这些 reducer 函数后面会被合并成一个。下面会介绍传入 reducer 函数需要满足的规则。
之前的文档曾建议使用 ES6 的
import * as reducers
语法来获得 reducer 对象。这一点造成了很多疑问,因此现在建议在reducers/index.js
里使用combineReducers()
来对外输出一个 reducer。下面有示例说明。
返回值
(Function):一个调用 reducers
对象里所有 reducer 的 reducer,并且构造一个与 reducers
对象结构相同的 state 对象。
注意
本函数设计的时候有点偏主观,就是为了避免新手犯一些常见错误。也因些我们故意设定一些规则,但如果你自己手动编写根 redcuer 时并不需要遵守这些规则。
每个传入 combineReducers
的 reducer 都需满足以下规则:
-
@H_403_3@所有未匹配到的 action,必须把它接收到的第一个参数也就是那个
state
原封不动返回。
@H_403_3@永远不能返回 undefined
。当过早 return
时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers
会抛异常。
@H_403_3@如果传入的 state
就是 undefined
,一定要返回对应 reducer 的初始 state。根据上一条规则,初始 state 禁止使用 undefined
。使用 ES6 的默认参数值语法来设置初始 state 很容易,但你也可以手动检查第一个参数是否为 undefined
。
实例:
const hello = (state = {userName: 'Hehe'},action) => { // 设置了初始值
switch (action.type) {
case 'USER_CHANGE':
return {
userName: action.userName
}
// 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
default:
return state
}
}
export default hello
异步action
学习到这里,我们所接触的下图上的所有实现,都是针对同步事件的。如果只是这样,那么我们肯定不能放心大胆的使用redux在我们的项目中,因为我们实际项目中,更多的都是异步事件。所以接下来,让我们来介绍一个复杂的场景,我们来看看redux是如何应用在大型复杂充满异步事件的场景中的。
我们仍然会遵守上图,这是我们的核心,不能改变,下面我们来看一个实际的例子,工资列表页面。
工资列表页面
也就是一个普通的通过网络请求,去请求列表数据的列表的展示。我们先来分析一下状态,列表页面的状态。
状态(state)
是一种数据结构,存储在store中的数据
异步加载的页面的状态:“加载中;加载成功,展示列表;加载失败” 这三种状态。我们给这三种状态来取一个名字,并设置0,1,2来顺序表示不同的状态。
loadingListStatus:0|1|2
我们主要做的是列表页的展示,那么还有一个最重要的数据结构就是列表数据,我们来取一个名字:
salaryList:[]
接下来我们再来分析一下,action,也就是事件。
事件
列表展示过程中的数据,也就是:“开始加载;加载成功;加载失败”这三个事件。其实整个过程和之前使用promise来实现的异步操作是一样的。我们是监听action,然后产生异步操作,执行dispatch方法,将数据结构保存到store中。
例子
我们来看一个获取列表的请求:
function fetchSalayList(subreddit) {
return dispatch => {
dispatch(loadingAction(subreddit))// 开始加载
return fetch(`http://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => { // 加载成功
dispatch(loadingSucessAction(subreddit,json))
},(error) => { // 加载失败
dispatch(loadingErroeAction(subreddit))
}
}
}
上述这种方式,完全符合我们的核心图表,并且实现了异步操作。
在异步操作这块,我们建议使用 redux-saga 中间件来创建更加复杂的异步 action。其中涉及到es6中的Generators可以在文档中查看。另外,还有 redux-saga的使用的一个例子可以看这里。
异步数据流
默认情况下,createStore()
所创建的 Redux store 没有使用 middleware,所以只支持 同步数据流。
你可以使用 applyMiddleware()
来增强 createStore()
。虽然这不是必须的,但是它可以帮助你用简便的方式来描述异步的 action。
像 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 ,来将处理流程带回同步方式)。
参考
-
@H_403_3@[React-Redux性能优化](https://segmentfault.com/a/1190000006120707)
@H_403_3@官网-中文
@H_403_3@redux的异步实现
@H_403_3@es6特性-Generators