学习在React项目中使用Redux

4,767 阅读10分钟

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

Redux简介

  1. Redux是什么?

    Redux is a predictable state container for JavaScript apps.

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

  2. 为什么要用Redux?

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

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

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

Redux

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

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 ComponentsContainer Components
PurposeHow things look (markup, styles)How things work (data fetching, state updates)
Aware of ReduxNoYes
To read dataRead data from propsSubscribe to Redux state
To change dataInvoke callbacks from propsDispatch Redux actions
Are writtenBy handUsually 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同。