新手专享:超详细的redux和react-redux手把手教程

9,127 阅读12分钟

前言

进了富途后,被要求用react来维护和开发新项目。这对于一直使用Vue的我来说需要好一段学习时间,在用react开发项目中,也用到redux,一开始我以为这玩意跟vuex差不多用法,结果却不是。在学习了一段时间后,通过这篇文章来总结一下自己对redux的理解。

一. Redux被创建的动机

为了解释redux,我们需要引入前端设计模型中的三个变量:ViewModelState

先解释 ViewModel

  • View: 视图界面,代表用户看到的html页面逻辑。
  • Model: 数据模型,代表与用户交互需要用到的数据。

通常他们在设计模型中的联系是这样子的:

image.png

ViewModel 之间相互耦合:

  • View-->Model: 用户与<input/>等标签进行交互,从而改变model中的数据。
  • Model-->View: 网络请求得到响应后,Model层数据发生更改,导致View内容随之改变。

如今的前端项目都是用组件化的思路去编写,每个组件在编写时都通常套用上面的设计模型进行开发。

而前端项目中,存在一些公共数据在多个组件需要用到,如UI主题用户信息(用户名、用户角色)。这些公共数据我们称之为State(状态)。其在设计模型中与ModelView的联系如下:

image.png

StateView Model 两者之间相互耦合:

  • View<-->State: 用户通过交互设置State中的网站UI主题,更改后反过来影响多个View
  • Model<-->State: 通过请求获取用户信息,然后更新State中的用户信息,多个Model的数据需要按照State中的用户信息进行更新。

从上可得出一个观点:State的变化会影响多个ModelView,因此管理State需要一定的要求。 在此可以套用redux官网里的原话加强此观点:

管理不断变化的 state 非常困难。如果一个 model 的变化会引起另一个 model 变化,那么当 view 变化时,就可能引起对应 model 以及另一个 model 的变化,依次地,可能会引起另一个 view 的变化。直至你搞不清楚到底发生了什么。state 在什么时候,由于什么原因,如何变化已然不受控制。 当系统变得错综复杂的时候,想重现问题或者添加新功能就会变得举步维艰。

在前端存在很多关于State管理的插件,比较广泛使用的是fluxredux。下面说一下redux的使用。

二. Redux在原生js中的使用

这里需要强调一下:Redux 和 React 之间没有关系。Redux 支持 React、Angular、Ember、jQuery 甚至纯 JavaScript。

这里先用原生js配合redux实现状态管理,主要是因为redux虽然一般配合react使用,但这样我们会错过认识几个原生API的机会。

1. Redux工作流的解释

首先要知道redux的工作流程是这样子的:

image.png

1. 使用action描述要执行的操作

action是用来给store描述操作类型的变量,其格式一般如下:

{
  type: 'ADD',
  // 可以有其他属性,但type必须存在
}

Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作。

通常通过dispatch(action)store传递action,然后store会把action连同state(也就是要更改的公共状态)传入到reducer中作处理。

2. 使用reducer处理action

reducer 就是一个纯函数,接收旧的 stateaction,读取action.type明确处理类型,处理完毕后返回新的 state作为新的状态记录到store上。

const reducer = (state=0,action)=>{
  switch (action.type) {
    case 'ADD':
      return state+1
    default:
      return state
  }
}

3. 使用subscribe订阅listener

通过subscribe(listener)添加一个变化监听器。每当 dispatch(action) 的时候就会执行,state中的一部分可能已经变化。你可以在回调函数里调用 getState() 来拿到当前 state

2. 示例:加减数字

实例中的页面有三个标签元素,从左到右分别是+按钮、显示数字的<span>标签、-按钮。按+按钮和-按钮分别让<span>标签中的数字递增和递减。如下所示:

add&sub.gif

github项目地址

目录结构如下:

image.png

store/action/number.js

定义需要dispatchaction

// 表示加1的action
export const ADD_NUMBER={
  type:'ADD',
}

// 表示减1的action
export const SUB_NUMBER={
  type:'SUB'
}

store/reducer/number.js

定义store中的reducer

// 处理action的reducer
const reducer = (state=0,action)=>{
  switch (action.type) {
    case 'ADD':
      return state+1
    case 'SUB':
      return state-1
    default:
      return state
  }
}

export default reducer

store/index.js

创建store

import { createStore } from 'redux'
import reducer from './reducer/number'

// 通过createStore函数创建store,传入参数中第一个是reducer,第二个是初始的state值(可省略)
const store = createStore(reducer,0)
export default store

index.html

页面代码

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>
    <button id="add">+</button>
    <span id="number"></span>
    <button id="sub">-</button>
  </div>
  <script src="./index.js"></script>
</body>
</html>

index.js

import store from './store'
import {ADD_NUMBER,SUB_NUMBER} from './store/action/number'

// 更新显示数字的span标签内容
function changeNumber(){
  document.querySelector('#number').textContent=store.getState()
}

// +按钮点击后执行的回调函数,内部通过dispatch分发加1的action
function add(){
  store.dispatch(ADD_NUMBER)
}

// -按钮点击后执行的回调函数,内部通过dispatch分发减1的action
function sub(){
  store.dispatch(SUB_NUMBER)
}

changeNumber()
// 通过subscribe注册监听器,当state变化时,会执行传入的回调函数
store.subscribe(()=>{
  changeNumber()
})

document.querySelector('#add').addEventListener('click',add)
document.querySelector('#sub').addEventListener('click',sub)

通过阅读以上例子结合Redux工作流的解释可以更清晰地了解整个redux的运行机制。

3. redux编写的三个原则

  1. 单一数据源: 整个应用只能有一个state,所有公共状态需存储到该state中,而这个state只能存储到一个store上。

  2. State是只读的: 不能通过类似store.getState().args = 1的直接更改State的语句来更改State的值。唯一改变 state 的方法就是触发 action

    此外,由于State是只读不可更改的,因此,reducerswitch语句块中,匹配到action.type经过处理后必须返回新的State,即:

    // 假设state为对象
    // 错误用法
    return Object.assign(state, {a:1,b:2})
    // 正确用法
    return {...state, a:1, b:2}
    // or
    return Object.assign({}, state, {a:1,b:2})
    

    action.type匹配不到switch语句中的case,即default情况下,返回传入的state

  3. 使用作为纯函数的Reducer来执行修改: 我们在编写Reducer函数时需要注意其必须为纯函数:

    简单来说,一个函数的返回结果只依赖于它的参数,并且在执行过程里面没有副作用,我们就把这个函数叫做纯函数。 永远不要在 reducer 里做这些操作:

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

4. 例子:加减数字加单位

下面来一个基于开头的加减数字来进行添加需求的例子,我们不仅要实现加减,还是实现单位切换,如下:

add&sub&unit.gif

github项目地址

目录结构不变,不过多出几个文件

image.png

store/actionstore/reducer中都建立unit.js文件分别存放涉及到单位更改的actionreducer。新建的store/reducer/index.js用于合并number.jsunit.js中的reducer

src/action/unit.js

// 用户可以通过调用该方法获取一个actio,传入的参数unit是指单位('cm','mm','m')
export const CHANGE_UNIT=(unit)=>({
  type:'CHANGE_UNIT',
  unit
})

CHANGE_UNIT这种用于生成action的函数,我们称之为action creator。不要混淆 actionaction creator 这两个概念。action 是一个信息的负载,而 action creator 是一个创建 action(包括同步action和异步action) 的工厂。

src/reducer/unit.js

const reducer = (state='m',action)=>{
  switch (action.type) {
    case 'CHANGE_UNIT':
      return action.unit
    default:
      return state
  }
}

export default reducer

src/reducer/index.js

import { combineReducers } from 'redux'
import number from './number'
import unit from './unit'

// 通过combineReducers合并reducer,之后无论有多少个类型的reducer,都可以通过该函数合并成一个reducer然后传入到createStore中
export default combineReducers({
  number,
  unit
})

combineReducers 辅助函数的作用是,把一个由多个不同 reducer 函数作为 value 的 object,合并成一个最终的 reducer 函数,然后就可以对这个 reducer 调用 createStore 方法。

合并后的 reducer 可以调用各个子 reducer,并把它们返回的结果合并成一个 state 对象。 由 combineReducers() 返回的 state 对象,会将传入的每个 reducer 返回的 state 按其传递给 combineReducers() 时对应的 key 进行命名。

最后,在store/index.jsindex.js以及index.html都有对应的变化如下所示:

store/index.js

import { createStore } from 'redux'
import reducer from './reducer'

//引入经combineReducers生成的reducer,且在createStore的第二个参数中把state的初始值弄成对象,以子级reducer的名字作为键名,存放初始值。
const store = createStore(reducer,{number:3,unit:'mm'})
export default store

index.js

import store from './store'
import {ADD_NUMBER,SUB_NUMBER} from './store/action/number'
import {CHANGE_UNIT} from './store/action/unit'

function changeNumber(){
  const {number,unit}=store.getState()
  document.querySelector('#number').textContent=`${number}${unit}`
}

function add(){
  store.dispatch(ADD_NUMBER)
}

function sub(){
  store.dispatch(SUB_NUMBER)
}

// 从'./store/action/unit'中引入生成涉及单位更改的action的函数,也就是CHANGE_UNIT。
function changeUnit(unit){
  store.dispatch(CHANGE_UNIT(unit))
}

store.subscribe(()=>{
  changeNumber()
})

changeNumber()
document.querySelector('#add').addEventListener('click',add)
document.querySelector('#sub').addEventListener('click',sub)

// 给按钮绑定事件
document.querySelector('#m').addEventListener('click',()=>{changeUnit('m')})
document.querySelector('#cm').addEventListener('click',()=>{changeUnit('cm')})
document.querySelector('#mm').addEventListener('click',()=>{changeUnit('mm')})

index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div>
    <button id="add">+</button>
    <span id="number"></span>
    <button id="sub">-</button>
  </div>
  <!--添加相应的元素按钮-->
  <div>
    <button id="m">m</button>
    <button id="cm">cm</button>
    <button id="mm">mm</button>
  </div>
  <script src="./index.js"></script>
</body>
</html>

三. Redux在React框架中的使用

我们把上面的例子用react实现。reactredux毫无关联,为了在以react为前端框架的项目中更灵活地使用过redux,我们使用react官方写的react-redux插件。实现的应用同样也是下面的效果:

add&sub&unit.gif

github项目地址

目录如下:

image.png

store文件夹的内容没变化,因为我们只是把项目从原生javascript转为react,对store部分的内容不需要变动。主要新增了components文件夹,用于存放react组件,我们把页面分成两部分,如图所示:

image.png

Number.jsx负责递增递减和显示内容的逻辑,Unit.jsx负责切换单位的逻辑,两个组件都最终都会被App.jsx引用,构成页面的内容,App.jsx的代码如下所示:

components/App.jsx

import Unit from './Unit'
import Number from './Number'
import  React  from 'react';

const App = ()=>(
    <div>
        <Number/>
        <Unit/>
    </div>
)

export default App

接下来看一下这两个React组件的代码:

components/Number.jsx

import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {ADD_NUMBER,SUB_NUMBER} from '../store/action/number'
import  React  from 'react';

// Number组件逻辑
const Number = ({add,number,unit,sub})=>{
    return (
        <div>
            <button onClick={add}>+</button>
            <span>{number}{unit}</span>
            <button onClick={sub}>-</button>
        </div>
    )
}

Number.propTypes={
    // 定义number和unit,将从store的state中注入
    number: PropTypes.number.isRequired,
    unit: PropTypes.string.isRequired,
    // 定义add和sub分别用于加和减
    add: PropTypes.func.isRequired,
    sub: PropTypes.func.isRequired,
}

const mapStateToProps = ({number,unit}) => ({
    number,unit
})

const mapDispatchToProps = (dispatch, ownProps) => ({
    // 加和减方法其实都是dispatch对应的action
    add: () => dispatch(ADD_NUMBER),
    sub: () => dispatch(SUB_NUMBER)
})

// 使用connect把store中的State状态和自定义的用于派发action的方法合并到组件中,组件可通过props调用
export default connect(mapStateToProps,mapDispatchToProps)(Number)

components/Unit.jsx

import PropTypes from 'prop-types'
import { connect } from 'react-redux'
import {CHANGE_UNIT} from '../store/action/unit'
import  React  from 'react';

// Unit组件逻辑
const Unit = ({changeUnit})=>{
    return (
        <div>
            <button onClick={()=>changeUnit('m')}>m</button>
            <button onClick={()=>changeUnit('cm')}>cm</button>
            <button onClick={()=>changeUnit('mm')}>mm</button>
        </div>
    )
}

Unit.propTypes={
    // 定义changeUnit用于转换单位
    changeUnit: PropTypes.func.isRequired,
}

const mapDispatchToProps = (dispatch) => ({
    changeUnit:(unit)=>dispatch(CHANGE_UNIT(unit))
})

export default connect(null,mapDispatchToProps)(Unit)

我们从components/Number.jsxcomponents/Unit.jsx两个文件中可以看出我们的写作顺序是:

  1. 编写对应功能的组件,理清逻辑知道哪些变量需要从外部导入,最好用prop-types编写对props数据的校验
  2. 如果需要用到storestate的数据时,要定义mapStateToProps。如果需要让组件有更改state里面的数据的能力时,要定义mapDispatchToProps
  3. 最后用connect方法把mapStateToPropsmapDispatchToProps注入到步骤一的功能中

这里我们来说说这个connect方法:

connect([mapStateToProps], [mapDispatchToProps], [mergeProps], [options])

一个用于把react组件和redux联系起来的方法。connect是一个柯里化函数,共通过两次传参来获取经处理后的react组件,第一次传参是传入mapStateToPropsmapDispatchToProps这类与store有关联的变量,第二次传参是传入纯react组件。

connect不会改变原来的组件类。反而返回一个新的已与 Redux store 连接的组件类。

我们先分析第一次传入的参数:

  • mapStateToProps:

    结构: (state, [ownProps])=>stateProps

    说明:

    如果定义该参数,组件将会监听 store 的变化。任何时候,只要 store 发生改变,mapStateToProps 函数就会被调用。该回调函数必须返回一个纯对象,这个对象(即stateProps)会与组件的 props 合并。从而有以下流程:

graph TD
store中的state发生变化 --> mapStateToProps被调用 --> 返回纯对象与组件的props合并 --> 组件props发生变化触发组件render函数执行 --> 组件UI更新

通过以上流程,实现了store中相关状态变化从而触发组件更新。

如果省略了mapStateToProps这个参数(如 components/Unit.jsx ),你的组件将不会监听 store。如果指定了该回调函数中的第二个参数 ownProps,则该参数的值为传递到组件的 props,而且只要组件的props有更新,mapStateToProps 也会被调用(例如:父组件传入到子组件中的参数有变化,导致子组件的props有变化,则会导致 mapStateToProps 被重新调用)。

  • mapDispatchToProps

    该参数有两种类型结构,对象和函数:

    1. Object: 如果传递的是一个对象,那么每个定义在该对象的函数都将被当作action creator,对象所定义的方法名将作为属性名;每个方法将返回一个新的函数,函数中dispatch方法会将action creator的返回值作为参数执行。这些属性会被合并到组件的 props 中。

    2. Function: (dispatch, [ownProps])=>dispatchProps

    如果传递的是一个函数,该函数将接收一个用于派发actiondispatch 函数,然后由你来决定如何返回一个对象,这个对象通过 dispatch 函数与 action creator 以某种方式绑定在一起。如果你省略这个 mapDispatchToProps 参数,默认情况下,dispatch 会注入到你的组件 props 中。如果指定了该回调函数中第二个参数 ownProps,该参数的值为传递到组件的 props,而且只要组件接收到新 propsmapDispatchToProps 也会被调用。

好了,关于components,也就是页面功能方面已经分析完了。我们继续分析接下来的代码,看index.htmlindex.jsx

index.html

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <!--直接把原本页面的内容换成一个div#app-->
  <div id="app"></div>
  <!-- <div>
    <button id="add">+</button>
    <span id="number"></span>
    <button id="sub">-</button>
  </div>
  <div>
    <button id="m">m</button>
    <button id="cm">cm</button>
    <button id="mm">mm</button>
  </div> -->
  <!--引入的文件格式从js改为jsx-->
  <script src="./index.jsx"></script>
</body>
</html>

index.jsx

import  React  from 'react';
import { render } from 'react-dom'
import { Provider } from 'react-redux'
import App from './components/App'
import store from './store'

render(
  <Provider store={store}>
    <App/>
  </Provider>,
  document.getElementById('app')
)

引用官方原话说一下<Provider store>这个API:

<Provider store> 使组件层级中的 connect() 方法都能够获得 store。正常情况下,你的根组件应该嵌套在 <Provider> 中才能使用 connect() 方法。

总结: 总的来说,react-redux提供的我们常用的就两个APIconnect<Provider store>。如果想了解更多,特别是connect有很多种传参方式,可去redux-react API详细阅读。

后记

本文从Redux被创建的动机出发,到Redux在原生js中的使用中通过例子让大家了解redux的大部分API(applyMiddleware我会另外写文章说明),再到Redux在React框架中的使用让大家了解react-redux提供了什么API让reactredux联系起来。

之后我下一篇文章会着重写一下redux的中间件,从而引出异步action,以及处理异步actionredux-thunkredux-saga插件的使用和原理。