React中的Redux

学习必备要点:

  1. 首先弄明白,Redux在使用React开发应用时,起到什么作用——状态集中管理
  2. 弄清楚Redux是如何实现状态管理的——store、action、reducer三个概念
  3. 在React中集成Redux:redux + react-redux(多了一个概念——selector)
  4. Redux调试工具:redux devtools
  5. redux相关很好用的插件:redux-saga的相关介绍

redux结构图

react-redux.png

其中红色虚线部分为redux的内部集成,不能显示的看到。

  • action:是事件,它本质上是JavaScript的普通对象,它描述的是“发生了什么”。action由type:string和其他构成。
  • reducer是一个监听器,只有它可以改变状态。是一个纯函数,它不能修改state,所以必须是生成一个新的state。在default情况下,必须但会旧的state。
  • store是一个类似数据库的存储(或者可以叫做状态树),需要设计自己的数据结构来在状态树中存储自己的数据。

Redux入门

Redux简介

Redux是一个状态集中管理库。

安装

npm install --save redux

附加包

多数情况下我们需要使用 React 绑定库开发者工具

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对象。

react-redux.png

上图是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 里做以下操作:

  • 修改传入参数;
  • 执行有副作用的操作,如 API 请求和路由跳转;
  • 调用非纯函数,如 Date.now()Math.random()

在后续的学习终将会介绍如何执行有副作用的操作,现在只需谨记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;    // 返回旧状态
    }
}

警告:

  1. 不要修改state。如果涉及多个状态时,可以采用对象展开运算符的支持,来返回一个新的状态。 假设我们的实例中还存在其它状态,但是我们只需要改变userName的值,那么上述示例我们可以采用以下方式返回新的状态: return { ...state, userName: action.payload }
  2. 在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),
    todos: todos(state.todos, 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联系到一起的对象,它有以下职责:

再次说明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工具查看到的是下图这样的:

store-tree.png

so,存储在store中的数据结构是由reducer确定的。

数据流

严格的单向数据流 是Redux架构的核心设计。这就意味着应用中所有的数据都遵循相同的生命周期,这样可以让应用变得更加可预测且容易理解。同时也鼓励做数据范式化,这样可以避免使用多个且独立的无法相互引用的重复数据。

Redux应用中数据的生命周期遵循以下4个步骤:

  1. 调用store.dispatch(action) Action 就是一个描述“发生了什么”的普通对象。比如: { type: 'CHANGE_USERNAME', payload: "Welcome to Redux" }; 我们可以在任何地方调用store.dispatch(action) 包括组件中、XHR回调中、甚至是定时器中。
  2. Redux store 调用传入的 reducer 函数。 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 前发生。
  3. 根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。 根 reducer 的结构完全由我们自己决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。
  4. Redux store 保存了根 reducer 返回的完整 state 树。 这个新的树就是应用的下一个state。所有订阅store.subscribe(listener) 的监听器都将被调用;监听器里可以调用store.getState() 获取当前的state。

示例: Hello App

如果想查看示例的源码,请查看这里。Hello App源码 开始之前我们需要清楚实际上Redux和React之间并没有关系。Redux支持React、Angular、Ember、jQuery甚至纯JavaScript。即便如此,Redux 还是和 ReactDeku 这类框架搭配起来用最好,因为这类框架允许你以 state 函数的形式来描述界面,Redux 通过 action 的形式来发起 state 变化。

下面我们将用React来开发一个Hello World的简单应用。

安装React Redux

Redux默认并不包含 React 绑定库,需要单独安装。

npm install --save react-redux

容器组件和展示组件

Redux 的 React 绑定库是基于 容器组件和展示组件相分离 的开发思想。而容器组件和展示组件大致有以下不同:

展示组件

容器组件

作用

描述如何展现内容、样式

描述如何运行(数据获取、状态更新)

是否能直接使用Redux

数据来源

props(属性)

监听Redux state

数据修改

从props中调用回调函数

向Redux派发actions

调用方式

手动

通常由React Redux生成

大部分的组件都应该是展示型的,但一般需要少数的几个容器组件把它们和Redux store连接起来。

技术上来说我们可以直接使用 store.subscribe() 来编写容器组件。但不建议这么做,因为这样写就无法使用 React Redux 带来的性能优化。同样,不要手写容器组件,我们直接使用 React Redux 的 connect() 方法来生成,后面会详细介绍。

需求分析

我们的需求很简单,我们只是想要展示hello + userName,默认为“Hello World!”,当我们在输入框中输入不同的值时,会显示不同的“hello,___”问候语,由此可以分析出该应用只有一个状态,那就是{ userName: '张三'}

展示组件

该应用只有一个展示组件HelloPanel:

  • HelloPanel 用于显示输入框及展示数据
    • userName: 要展示的数据
    • onChange(userName) : 当输入值发生变化时调用的回调函数

该组件之定义外观并不涉及数据从哪里来,如果改变它,传入什么就渲染什么,如果你把代码从Redux迁移到别的架构,该组件可以不做任何改动直接使用。

容器组件

还需要一个容器组件来把展示组件连接到Redux。例如HelloPanel 组件需要一个状态类似HelloApp的容器来监听Redux store变化并处理如何过滤出要展示的数据。

HelloApp 根据当前显示状态来对展示组件进行渲染。

组件编码

  • Action创建函数 action.js export function changeUserName(userName) { return { type: 'CHANGE_USERNAME', payload: userName, }; }
  • 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; // 返回当前状态 } }
  • 展示组件 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> ); }
  • 容器组件 使用 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 HelloReducer from './reducers'

let store = createStore(HelloReducer)

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 都需满足以下规则:

  • 所有未匹配到的 action,必须把它接收到的第一个参数也就是那个 state 原封不动返回。
  • 永远不能返回 undefined。当过早 return 时非常容易犯这个错误,为了避免错误扩散,遇到这种情况时 combineReducers 会抛异常。
  • 如果传入的 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是如何应用在大型复杂充满异步事件的场景中的。

react-redux.png

我们仍然会遵守上图,这是我们的核心,不能改变,下面我们来看一个实际的例子,工资列表页面。

工资列表页面

也就是一个普通的通过网络请求,去请求列表数据的列表的展示。我们先来分析一下状态,列表页面的状态。

状态(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-thunkredux-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 ,来将处理流程带回同步方式)。

参考

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏崔庆才的专栏

正则表达式中零宽断言的用法

了解了正则表达式,想必一般情况下的匹配都不会出现什么问题,但是如果一些特殊情况,可能需要用到一些更高级的正则表达式匹配操作,本节我们来说明一下正则表达式的一个较...

2934
来自专栏小樱的经验随笔

IPython使用学习笔记

学习《利用python进行数据分析》第三章 IPython:一种交互式计算和开发环境的笔记,共享给大家,同时为自己作为备忘用。 ? 安装ipython用pip即...

4065
来自专栏数据库

基于关系型数据库的App Inventor网络应用(3)

第三节 初识Node-RED 开发环境简介 如图8所示,整个浏览器窗口被划分为四个部分: (1) 顶部黑色通栏,左侧显示Node-RED的LOGO,右侧显著位置...

3447
来自专栏九彩拼盘的叨叨叨

学习纲要:异步流程处理

修改上面的代码,用 Promise,async/await,事件发布订阅 这几种方式实现下面的需求

952
来自专栏柠檬先生

zepto 基础知识(6)

101.$.ajax   $.ajax(options) 类型:XMLttpRequest   执行Ajax请求。他可能是本地资源,或者通过支持...

29210
来自专栏程序员宝库

10 种最常见的 Javascript 错误

英文:SKOWRONSKI 译文:elevenbeans elevenbeans.github.io/2018/02/05/top-10-javascript...

3808
来自专栏逸鹏说道

Python3 与 C# 并发编程之~ 线程篇2

其实以前的 Linux中是没有线程这个概念的, Windows程序员经常使用线程,这一看~方便啊,然后可能是当时程序员偷懒了,就把进程模块改了改(这就是为什么之...

1974
来自专栏阮一峰的网络日志

Bookmarklet编写指南

前一段日子,我写了两个Bookmarklet----"短网址生成"和"短网址还原"。 它们用起来很方便,除了我本人之外,其他朋友也在用。第一次发布Bookmar...

4749
来自专栏GIS讲堂

lzugis——Arcgis Server for JavaScript API之POI

POI(Point Of Interest),感兴趣点,其实呢,严格意义上说应该不是POI,但是单位就这样叫了,我也就这样叫了,其实现的功能大致是这样的:用过百...

1092
来自专栏不止是前端

TS+React+Router+Mobx+Koa打造全栈应用

4877

扫码关注云+社区

领取腾讯云代金券