Redux

444 阅读7分钟

什么是redux?

Javascript 状态容器,提供可预测化的状态管理

好处:

  • 可以通过redux操作对象中的属性,借助react来更新操作DOM
  • 数据状态更加易于维护,能快速和准确定位到问题

redux 核心概念及数据流程

React 结合 Redux

在react 中不使用redux 遇到的问题

在react中通信的数据流是单向的,顶层组件通过props向下层组件传递数据,然而下层组件不能向上层组件传递数据,要实现下层组件修改数据,需要上层组件向下传递一个修改数据的方法,当项目变得越来越庞大时,组件之间传递数据变得越来越复杂

在react中加入 redux 的好处

使用redux ,由于Store 独立于组件, 使得数据管理独立于组件,解决了组件与组件之间传递数据的困难的问题

案例体验

环境准备

  • 全局安装create-react-app

npm install create-react-app -g

  • 使用create-react-app 脚手架创建项目名为react-redux-guide

create-react-app react-redux-guide

删除掉脚手架生成的多余的文件,以及删除掉现有的APP.js 和 index.js 中引用的语句,项目结构如下:

现在我们开始,实现一个计数器的案例。

//----index.js-------
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
// 3. 存储默认状态
const initialState = {
  count: 0
}
// 2. 创建 reducer 函数
function reducer(state = initialState, action) {
  if (action.type === 'increment') {
    state.count = state.count + 1
  } else if (action.type === 'decrement') {
    state.count = state.count - 1
  }

  return state
}
// 1. 创建 store 对象
const store = createStore(reducer)
// 4. 定义 action
const increment = {type: 'increment'}
const decrement = {type: 'decrement'}
// 5. 定义一个计数器组件
function Counter () {
  return (
    <div>
      <button onClick= {() => store.dispatch(decrement)} >-</button>
      <span>{store.getState().count}</span>
      <button onClick= {() => store.dispatch(increment)}>+</button>
    </div>
    
  )
}
store.subscribe(() => {
  // console.log(store.getState())
  // 重新渲染
  ReactDOM.render(<Counter/>, document.getElementById('root'))
})
ReactDOM.render(
    <Counter />,
  document.getElementById('root')
);

实现效果:

虽然上面的代码已经实现了我们的基本功能,但是我们的代码都挤在了一个文件中,因此,我们做一个拆分。

我们在src 下新建一个文件夹component,借着创建一个组件Counter.js。项目结构如下:

Counter.js:

import React from 'react'
function Counter () {
    const increment = { type: 'increment' }
    const decrement = { type: 'decrement' }
    return (
        <div>
            <button onClick={() => store.dispatch(decrement)} >-</button>
            <span>{store.getState().count}</span>
            <button onClick={() => store.dispatch(increment)}>+</button>
        </div>

    )
}

export default Counter

此时我们要如何获取store 呢?

Provider 组件 和 connect 方法

为了解决上文中无法获取到 store ,我们引入了react-redux模块中的Provider 组件和connect 方法。

Provider组件能够将store放到全局中,供其它组件调用。

connect 方法能够帮我们实现:

  1. 订阅store,当store中的状态发生改变时,会帮我们重新渲染组件

  2. 获取store中的状态,映射给组件中的props属性

  3. 可以获取到dispatch 方法

我们先来看看Provider组件的使用:

//--------index.js-----
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import Counter from './component/Counter'
// 3. 存储默认状态
const initialState = {
  count: 0
}
// 2. 创建 reducer 函数
function reducer(state = initialState, action) {
  if (action.type === 'increment') {
    return {count: state.count + 1 } 
  } else if (action.type === 'decrement') {
    return {count: state.count - 1 }
  }

  return state
}
// 1. 创建 store 对象
const store = createStore(reducer)


ReactDOM.render(
  // 通过Provider组件,将store放到全局中去
    <Provider store = { store }><Counter /></Provider>,
  document.getElementById('root')
);

接着我们来看看 connect 方法:

// -------Counter.js-------
import React from 'react'
import { connect } from 'react-redux'
function Counter ({count, dispatch}) {
    const increment = { type: 'increment' }
    const decrement = { type: 'decrement' }
    return (
        <div>
            <button onClick={() => dispatch(decrement)}>-</button>
            <span>{count}</span>
            <button onClick={() =>dispatch(increment) }>+</button>
        </div>

    )
}
const mapStateToProps = state => ({
    count: state.count
})
export default connect(mapStateToProps)(Counter)

到此,我们还有一个问题需要处理,看下面代码

return (
        <div>
            <button onClick={() => dispatch(decrement)}>-</button>
            <span>{count}</span>
            <button onClick={() =>dispatch(increment) }>+</button>
        </div>

    )

button 的点击事件直接绑定了逻辑代码,显然是不易于维护的,因此我们需要进一步优化一下代码。

定义一个函数mapDispatchToProps,返回一个包含事件处理函数的对象:

const mapDispatchToProps = dispatch => ({
    increment () {
        dispatch({type: 'increment'})
    },
    decrement () {
        dispatch({type: 'decrement'})
    }
})

mapDispatchToProps 作为connect方法的第二参数

export default connect(mapStateToProps, mapDispatchToProps)(Counter)

更改一下函数组件

// ----Counter.js--------
import { bindActionCreators } from 'redux'
function Counter ({count, increment, decrement}) {
    return (
        <div>
            <button onClick={decrement}>-</button>
            <span>{count}</span>
            <button onClick={increment}>+</button>
        </div>

    )
}

bindActionCreators

const mapDispatchToProps = dispatch => ({
    increment () {
        dispatch({type: 'increment'})
    },
    decrement () {
        dispatch({type: 'decrement'})
    }
})

上面的代码中increment 和 decrement 方法之间重复调用了 dispatch,是属于同类代码,因此,可以优化一下:

const mapDispatchToProps = dispatch => ({
    ...bindActionCreators({
        increment () {
            return {type: 'increment'}
        },
        decrement () {
            return {type: 'decrement'}
        },
    },dispatch)
})

你可能会觉得,这不是越优化,代码量变得越多了吗? 我们还没结束呢

按照下面的文件结构创建Counter.action.js

// Counter.action.js
export const increment = () => ({type: 'increment'})
export const decrement = () => ({type: 'decrement'})

在Counter.js中以别名的方式,全部导入

// Counter.js
import * as counterActions from '../store/actions/Counter.action'

const mapDispatchToProps = dispatch => bindActionCreators(counterActions,dispatch)

至此,我们通过使用bindActionCreators, 将代码优化了

拆分reducer

文件目录结构图:


// Counter.reducer.js
// 存储默认状态
const initialState = {
    count: 0
  }
  // 创建 reducer 函数
  function reducer(state = initialState, action) {
    if (action.type === 'increment') {
      return {count: state.count + 1 } 
    } else if (action.type === 'decrement') {
      return {count: state.count - 1 }
    }
  
    return state
  }

  export default reducer
  
  // index.js
  import { createStore } from 'redux'
  import reducer from './Counter.reducer'

  export  const store = createStore(reducer)

Redux 中间件

中间件本质上是一个函数,允许我们扩展redux的应用程序。

加入中间件后的redux的工作流程:

开发Redux中间件

模板代码

export default store => next => action => {}

我们开发一个打印日志的组件logger:

const logggerMiddleware = store => next => action => {
    console.log(store)
    console.log(action)
    // 调用next, 执行下一个中间件或者 reducer
    next(action)
}
export default logggerMiddleware

注册中间件

中间件在开发完成后,只有被注册才能生效


import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducer/Root.reducer'
import loggerMiddleware from './middleware/logger'

export  const store = createStore(rootReducer, applyMiddleware(loggerMiddleware))

调用运行结果:

开发异步中间件

我们接着使用上文中计数器的案例,假设现在我们的计数器要实现延时2秒,再增加或减小。

创建一个异步中间件thunk.js:

const thunkMiddleWare = store => next => action => {
    if (action.type === 'increment' || action.type === 'decrement') {
        setTimeout(() => {
            next(action)
        }, 2000);
    }
    // if (typeof action === 'function') {
    //     return action(store.dispatch)
    // }
    // next(action)
}
export default thunkMiddleWare

虽然,这种方式能够达到我们的目的,但是很不灵活。假设还有另外一个需求,点击modal中的显示按钮,延迟2秒显示modal窗口。这时候我们就需要再去创建一个中间件,去实现这个功能。要是异步的功能有N 个,那我们难道就要创建N个中间件吗? 因此,上述方法不适用,我们需要另寻它法。

要解决上面遇到的问题,我们需要思考一下如何实现一个灵活通用的中间件:

  1. 当前这个中间件函数不关心你想执行什么样的异步操作 只关心你执行的是不是异步操作
  2. 如果你执行的是异步操作 你在触发action的时候 给我传递一个函数 如果执行的是同步操作 就传递action对象
  3. 异步操作代码要写在你传递进来的函数中
  4. 当前这个中间件函数在调用你传递进来的函数时 要将dispatch方法传递过去

按照上面的思路,我们更改一下thunk.js代码:

const thunkMiddleWare = store => next => action => {
   
    if (typeof action === 'function') {
        return action(store.dispatch)
    }
    next(action)
}

增加一个处理异步增加的函数:

// Counter.action.js
export const increment_async = payload => dispatch => {
    setTimeout(() => {
        dispatch(increment(payload))
    }, 2000)
}

常用redux 中间件

redux-thunk

使用步骤:

  1. npm install redux-thunk
  2. import thunk from 'redux-thunk'
  3. 注册 applyMiddleware

export const store = createStore(RootReducer, applyMiddleware(thunk));

redux-saga

redux-saga 解决的问题: 可以将异步操作从Action Creator 文件中抽离出来, 放在一个单独的文件中。

redux-saga 的使用:

  • 下载

npm install redux-saga

  • 创建redux-saga 中间件
import createSageMiddleware from 'redux-saga'
// 创建sagaMiddleWare中间件
const sagaMiddleWare = createSageMiddleware()
  • 注册sagaMiddleSaga
createStore(rootReducer,
    // 注册中间件
    applyMiddleware(sagaMiddleWare)
    )
  • 使用saga接受action执行异步操作
import { takeEvery, put, delay } from 'redux-saga/effects';
import { increment } from '../actions/counter.actions';
import { INCREMENT_ASYNC } from '../const/counter.const';

// takeEvery 接收 action
// put 触发 action

function* increment_async_fn (action) {
  yield delay(2000);
  yield put(increment(action.payload))
}

export default function* counterSaga () {
  // 接收action
  yield takeEvery(INCREMENT_ASYNC, increment_async_fn)
}
  • 启动saga
// 
// 启动saga
sagaMiddleWare.run(counterSaga)

all 合并多个saga

结合上文中的案例,我们现在要处理modal 的异步显示,处理过程:

  • 创建一个modal.saga.js,代码如下:
// modal.saga.js
import { takeEvery, put, delay } from 'redux-saga/effects'
import { SHOW_MODAL_ASYNC } from '../constant/Counter.constant'
import { show } from '../actions/Modal.action'
function* show_modal_fn () {
    yield delay(2000)
    yield put(show())
}
export default function * modalSaga () {
    yield takeEvery(SHOW_MODAL_ASYNC, show_modal_fn)
}

  • 创建 root.saga.js
import { all } from 'redux-saga/effects'
import counterSaga from './counter.saga'
import modalSaga from './modal.saga'
export default function * rootSaga() {
    yield all([
        counterSaga(),
        modalSaga()
    ]) 
}
  • 启动 rootSaga
sagaMiddleWare.run(rootSaga)

redux-action

redux-action 解决的问题:

redux 流程中大量的样板代码读写很痛苦,使用redux-action 可以简化Action 和 reducer 的处理

redux-action 的使用:

  • 下载

npm install redux-action

  • 创建Action
import  { createAction } from 'redux-actions'
// 创建Action 
export const increment = createAction('increment')
export const decrement = createAction('decrement')
  • 创建reducer
import { handleActions as createReducer } from 'redux-actions'
import { decrement, increment } from '../actions/counter.createAction'

// 存储默认状态
const initialState = {
    count: 0
  }
const handleIncrement = (state) => {
    return {
        count: state.count + 1
    }
}
const handleDecrement = (state) => {
    return {
        count: state.count - 1
    }
}
export default createReducer({
    [increment]: handleIncrement,
    [decrement]: handleDecrement
}, initialState)