学习在React项目中使用Redux

Redux是用来管理JavaScript应用的状态的。官方文档地址:Redux。本文讲述如何在React中使用Redux,重点偏向Redux。若是须要了解React,能够参考React官网或者React入门前一阵入门React时写的文|ω・))。javascript

Redux简介

  1. Redux是什么?java

    Redux is a predictable state container for JavaScript apps.react

    Redux是一个用在JavaScript应用中的可预测状态容器(这里的状态指的是存放信息的对象)。Redux是用来对状态进行统一管理的,整个应用的状态以一棵对象树的方式被存放在store中。状态是只读的,惟一改变状态的方式是dispatch actions,因此状态的改变是可预测的。经过dispatch改变store中的state,经过subscribe监听状态的改变,经过getState来拿到当前的状态,这样数据的处理都集中在Redux中。git

  2. 为何要用Redux?github

    想象这样的场景,组件一、组件二、组件三、组件五、组件7(下图标绿的组件)中都须要使用一些数据,由于在React中,数据是单向流动的,因此在组件7中改变数据了,就须要组件7通知组件4改变数据,组件4再通知组件1改变数。在组件1中改变数据以后再层层往下传递数据。这还只是下图这样结构比较简单的应用,若是应用更复杂,那处理起来就是一团乱麻了。因此咱们须要用Redux来对数据(状态)进行统一的管理,当须要改变某数据时,直接改变store中的数据,当要获取数据时,也直接从store中拿数据。json

    图-1 没使用Redux时的处理流程
    图-2 使用Redux时的处理流程
  3. React和Redux有什么关系?redux

    听见Redux这个名字的时候,由于和React同样都是以Re开头的,可能会产生Redux是专门为React解决问题的一个工具这样的误解,但实际上React和Redux啥关系也没有,是两个独立的工具。你能够将Redux和React结合起来使用,也能够将Redux和jQuery,甚至Redux和JavaScript结合使用。app

Redux

Redux中有几个必须知道的概念:action、reducer、store。下文中提到的state(状态)指的是存放信息的对象,这个对象是放在Redux的store中的。dom

actions

actions是从应用到store的数据的载体。当咱们须要改变store中的数据的时候,须要dispatch actions。action必须有一个type属性,代表即将执行的action的类型,type一般被定义为一个字符常量。action对象中的其余属性是携带的信息。异步

例如:

{
    type: ADD_NUMBER,
    number: 1
}
复制代码

action creator是建立action的函数,简单地返回action,它能根据传入的参数设置action的属性值。

例如:

const ADD_NUMBER = 'ADD_NUMBER'
export const addNumber = number => ({
  type: ADD_NUMBER,
  number
})
复制代码

reducers

reducer是纯函数,它是用来指明当action发送(dispatch)到store的时候,state是如何改变的。它的第一个参数为前一个state,第二个参数为action对象,返回的值是下一个state。

纯函数:

  1. 传入相同的参数会返回相同的结果。
  2. 执行纯函数不会形成反作用。(这里的反作用指的是函数改变了做用域以外的状态值,好比函数外部定义了变量a,在函数内部改变了变量a的值。)

在reducer中不能作如下操做:改变它的参数;形成反作用;调用非纯函数(好比Math.random())。

如下的caculate函数是一个简单的reducer。若是createStore函数没有传入第二个参数(state的初始值),那么首次调用caculate的时候,实参state的值为undefined。因此给state参数设置了一个默认值0。

function caculate (state = 0, action) {
  const { number } = action
  switch (action.type) {
    case 'ADD_NUMBER':
      return state + number
    case 'SUBTRACT_NUMBER':
      return state - number
    case 'MULTIPLY_NUMBER':
      return state * number
    default: 
      return state
  }
}
复制代码

当应用比较大的时候,通常的作法是使用多个小的reducers,每一个reducer处理特定的内容。而后使用Redux提供的combineReducers将reducers合并为一个reducer,合并后的reducer做为createStore的参数来建立store实例。下文介绍完store以后,会给出一个简单的例子来讲明这部份内容。

store

store将actions和reducers结合起来,store的功能是:

  1. 保存应用的state。
  2. 经过getState()拿到state。
  3. 经过dispatch(action)更新state。
  4. 经过subscribe(listener)注册监听器。listener是一个函数,当发送action的时候会执行listener。
  5. 执行subscribe(listener)会返回 一个函数,调用这个函数可以注销监听器。

一个应用中只能有一个store,想要拆分数据的话,须要使用多个reducers。

如下代码中的caculate是上文建立好的reducer,addNumber是上文定义的action creator。

import { createStore } from 'redux'

const store = createStore(caculate)
const listener = () => console.log(store.getState()) // 打印当前状态
const unsubscribe = store.subscribe(listener)

store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))

unsubscribe()
复制代码

使用store.subscribe(listener)注册了监听器,当store.dispatch()发送action到store,reducer将state改变以后,监听器函数listener会执行,打印出如下结果:

1
3
6
复制代码

我须要另外一个reducer来处理标签切换,如下是相应的action creatore和reducer。

const SELECT_TAB = 'SELECT_TAB'
const selectTab = tab => ({
  type: SELECT_TAB,
  tab
}) 
function switchTab (state = 'CACULATE', action) {
  switch (action.type) {
    case 'SELECT_TAB':
      return action.tab
    default:
      return state
  }
}
复制代码

如今就有了两个reducer。使用combineReducers将reducers合并为一个根reducer,将根reducer做为createStore的第一个参数,初始的state值做为createStore的第二个参数来建立store。

let state = {
  caculate: 0,
  switchTab: 'CACULATE'
}
const reducer = combineReducers({ caculate, switchTab })
const store = createStore(reducer, state)
const listener = () => console.log(store.getState())
const unsubscribe = store.subscribe(listener)

store.dispatch(addNumber(1))
store.dispatch(addNumber(2))
store.dispatch(addNumber(3))

unsubscribe()
复制代码

combineReducers所作的事情是生成一个函数,调用这个函数时,会执行每一个reducer,并将执行的结果从新整合为一个对象,做为新的state。当调用store.dispatch(addNumber(1))时,两个reducer(caculate和switchTab)都会被调用,它们执行的结果会组成一个新的state树。

代码段中的:

const reducer = combineReducers({ caculate, switchTab })
复制代码

和如下代码段的效果是同样的,

function reducer (state = {}, action) {
  return {
    caculate: caculate(state.caculate, action),
    switchTab: switchTab(state.switchTab, action)
  }
}
复制代码

只是combineReducers会有更多的处理操做。

在React项目中使用Redux

react-redux插件是专门用于在React中使用redux的,react-redux官网地址

React Redux将展现组件(presentational components)和容器组件(container components)分离了。

Redux官网中有二者的对比表格:

Presentational Components Container Components
Purpose How things look (markup, styles) How things work (data fetching, state updates)
Aware of Redux No Yes
To read data Read data from props Subscribe to Redux state
To change data Invoke callbacks from props Dispatch Redux actions
Are written By hand Usually generated by React Redux

容器组件能将展现组件和Redux联系起来。通常使用React Redux提供的connect方法生成容器组件。

React Redux

  1. connect 方法

    调用connect方法会返回一个包装函数,这个函数接收组件并返回一个新的包装组件。这里的包装组件便是上文说到的容器组件,容器组件能将展现组件和Redux联系起来。

    connect的使用方法是:

    connect(mapStateToProps? mapDispatchToProps?, mergeProps?, options?)(组件)
    复制代码
    • mapStateToProps,这个参数是一个函数,当store更新的时候,mapStateToProps就会被调用,若是不想订阅store的更新,就在这个位置传null或undefined。从函数名可知,这个参数的做用是将Redux store的state映射到React组件的属性。mapStateToProps函数的格式是这样的:(state, ownProps?) => Object,state指Redux store中的state,第二个参数是组件本身的属性。这个函数执行后返回的对象会和组件的属性合并。
    • mapDispatchToProps,这个参数能够是一个对象或者函数。从函数名可知,这个参数的做用是将Redux store的dispatch映射到React组件的属性,若是没有传入这个参数,那么组件会默认收到dispatch属性,这个dispatch属性的值是Redux store的dispatch方法。若是传入的参数是一个函数,那么这个函数是这样的:(dispatch, ownProps?) => Object,它的第一个参数是Redux中的dispatch,第二个参数是组件本身的属性。这个函数返回一个对象,这个对象的每一个属性值都是一个函数,这个对象会合并到组件的属性中。
  2. Provider组件

    全部的容器组件都须要可以访问Redux的store,若是不使用Provider组件,就只能在全部容器组件的属性中传入store,假如在组件树中深层嵌套了容器组件,可能有的展现组件也须要传入store属性。可是使用Provider组件包裹上应用根组件后,应用中的全部容器组件就都能访问到Redux的store了。

举个例子,定义容器组件的时候这样写:

import { connect } from 'react-redux'
import { addNumber, subtractNumber, multiplyNumber } from '../actions'
import Caculation from '../components/Caculation'

const mapStateToProps = state => ({
  caculate: state.caculate
})

const mapDispatchToProps = dispatch => ({
  plus: number => dispatch(addNumber(number)),
  subtract: number => dispatch(subtractNumber(number)),
  multiply: number => dispatch(multiplyNumber(number))
})

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Caculation)
复制代码

那么展现组件Caculation的属性中就会有caculate,plus,subtract,multiply这几个属性,其中caculate是state.caculate;plus,subtract,multiply是三个函数,调用函数的时候能进行dispatch操做。

异步获取信息

Redux只支持同步数据流,若是想要使用异步actions,须要使用applyMiddleware加强createStore。使用redux-thunk中间件可以发起同步或异步的actions,而且能够设计知足某种条件才dispatch actions。使用Redux Thunk以后,action creator能够返回一个函数,这个函数的第一个参数是dispatch,第二个参数是getState。 首先下载好redux-thunk,并使用applyMiddleware加强createStore:

import { createStore, applyMiddleware } from 'redux'
import thunk from 'redux-thunk'
...

const store = createStore(rootReducer, applyMiddleware(thunk))
复制代码

在thunk中间件的帮助下,就能够像下面这样使用异步action 了。在请求开始和请求结束的地方都触发一个action,在请求开始的地方触发的action能够用来展现一个加载中的loading图等。这里只简单地在请求开始时触发了action,没有作后续的使用。

const requestLeaderBoard = () => ({
  type: REQUEST_LEADERBOARD
})

const receiveLeaderBoard = (json) => ({
  type: RECEIVE_LEADERBOARD,
  leaderboard: json.data
})

export function fetchLeaderBoard () {
  return dispatch => {
    dispatch(requestLeaderBoard())
    return fetch(`这里用一个get请求的地址`)
      .then(res => res.json())
      .then(json => dispatch(receiveLeaderBoard(json)))
  }
}
复制代码

为了学习redux写了一个练习项目:源码地址

遇到的问题

  1. 定义的reducer执行了“多余”的次数。

    • 状况1:没有使用combineReducers时,reducer会多执行1次,action的type为:@@redux/INITd.c.p.m.e.7
    • 状况2:使用combineReducers以后,每一个reducer都多执行了3次,action的type分别是:@@redux/INITf.1.c.7.u.x@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9@@redux/INITf.1.c.7.u.x。有3个多出的reducer调用。

    状况2的问题请教了可爱的同事杨老板,杨老板找到了相应的源码给出了解答,而后我又按照相同的方式在源码中找了下,获得了状况1的答案。

    (1)状况1出现的缘由:

    当使用createStore建立好一个store以后,Redux会dispatch一个初始化action(以下),以保证每一个reducer返回它们的默认state。

    dispatch({ type: ActionTypes.INIT })
    复制代码

    (2)状况2出现的缘由:

    执行第一个action@@redux/INITf.1.c.7.u.x是由于Redux在执行combineReducers的过程当中调用了一遍reducer,做用是检查调用reducer返回的初始state是否是undefined,若是是undefined会抛出一个错误。

    const initialState = reducer(undefined, { type: ActionTypes.INIT })
    复制代码

    第二个action@@redux/PROBE_UNKNOWN_ACTIONz.n.g.2.k.9也是在combineReducers执行的过程当中进行处理的,目的是检查action的名称和Redux内私有的名称是否重复,若是重复,就抛出一个错误。

    第三个action和状况1同。

相关文章
相关标签/搜索