redux最佳实战(一)

526 阅读7分钟
redux核心知识
简介

redux是一个JavaScript状态容器,提供可预测化的状态管理。

redux主要有四个核心概念

  • Store 存储状态的容器,JavaScript对象
  • View 视图,html页面
  • Action 对象,描述对状态进行怎样的操作
  • Reducers 函数,操作并返回新的状态
案例

接下来通过redux实现一个简单的计数器,加深对redux的认识:

<button id="plus">+</button>
<span id="count">0</span>
<button id="minus">-</button>
// 3. 存储默认状态
var initialState = {
  count: 0
}
// 2. 创建 reducer 函数
function reducer (state = initialState, action) {
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1}
    default:
      return state;
  }
}
// 1. 创建 store 对象
var store = Redux.createStore(reducer);

// 4. 定义 action
var increment = { type: 'increment' };
var decrement = { type: 'decrement' };

// 5. 获取按钮 给按钮添加点击事件
document.getElementById('plus').onclick = function () {
  // 6. 触发action
  store.dispatch(increment);
}

document.getElementById('minus').onclick = function () {
  // 6. 触发action
  store.dispatch(decrement);
}

// 7. 订阅 store
store.subscribe(() => {
  // 获取store对象中存储的状态
  // console.log(store.getState());
  document.getElementById('count').innerHTML = store.getState().count;
})

首先通过Redux.createStore创建redux实例,参数就是我们的reducer函数,reducer函数有两个参数,一个就是我们state仓库,第二个就是我们的action。通过上图,我们看到要更新视图,首先要dispatch来触发action,于是第五步、第六步就是通过点击函数来触发action,通过action去触发reducer更改我们的state仓库;仓库更改了,就需要更新视图,就需要subscribe函数出场了,在subscribe中对视图进行更新。

这个demo还是非常简单的,使用redux来更改数据,有点杀鸡用牛刀的感觉。在一个较大的项目中,数据管理一直都是一个难点,比如在react项目中,react组件间的通信都是单向数据流的,如果涉及层级过多,传参就会很困难,使用context如果数据过多,就会变得很难维护,这个时候redux就排上了用场。由于Store独立于组件,是的数据管理同样独立于组件,这就使得数据管理变得有迹可循,后期容易维护,组件通信也容易了许多。

react+redux

上面为了了解redux,流程图画的较为的简单,接下来再重新介绍下redux工作流程

  • 组件通过dispatch方法触发action
  • Store接受action并将Action分发给Reducer
  • Reducer根据Action类型对状态进行更改并将更改后的状态返回给Store
  • 组件订阅了Store中的状态,Store中的状态更新会同步到组件中

接下来使用react来重构上述的计数器

//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
// import App from './App';

import {createStore} from 'redux';

const initialState = {
  count:0
}

function reducer(state = initialState,action){
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1}
    default:
      return state;
  }
}

const store = createStore(reducer)

const increment = {type:'increment'}
const decrement = {type:'decrement'}

function Counter(){
  return (
    <div>
      <button onClick={() => store.dispatch(increment)}>+</button>
      <span>{store.getState().count}</span>
      <button onClick={() => store.dispatch(decrement)}>-</button>
    </div>
  )
}

store.subscribe(() => {
  root.render(
    <Counter/>
  );
})

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Counter/>
);


这种写法实际上有个很大的问题,平常我们封装的组件都是在一个文件夹里单独导入的,在单独的组件内如何拿到Store实例,以及触发subscribe函数,这就需要一个插件react-redux出场了。这个插件中主要暴露了一个组件和一个方法,Provider组件和contect方法。

  • Provider组件,这个组件将我们的Store实例放到全局作用域上,让所有的组件都能够引用,使用方法如下:

    <Provider store={store}>
       <Counter/>
    </Provider>
    //需要包裹项目的所有的组件
    
  • contect方法主要有以下几个功能

    • 会订阅store,当store中的状态发生更改时,会重新渲染组件
    • 可以获取store中的状态,将状态通过组件的props属性映射给组件
    • 可以获取dispatch方法

接下来是代码编写:

//index.js
import React from 'react';
import ReactDOM from 'react-dom/client';
// import App from './App';

import {createStore} from 'redux';
import { Provider } from 'react-redux';

import Counter from './components/Counter.js'

const initialState = {
  count:0
}

function reducer(state = initialState,action){
  switch (action.type) {
    case 'increment':
      return {count: state.count + 1};
    case 'decrement':
      return {count: state.count - 1}
    default:
      return state;
  }
}

const store = createStore(reducer)


const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
   <Counter/>
</Provider>
);
// /components/Counter.js
import React from "react";
import {connect} from 'react-redux'

const increment = {type:'increment'}
const decrement = {type:'decrement'}

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

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

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

// connect  第一个参数  就是state仓库  组件中的属性可以通过props.state拿到state
//          第二个参数  是一个函数 返回一个对象  该对象的属性都可以通过props拿到
export default connect(mapStateToProps,mapDispatchToProps)(Counter)

首先通过函数createStore创建一个store实例,参数就是reducer函数,用以更改state仓库;然后用Provider组件包裹我们的组件,这样我们的业务组件就可以拿到store实例;在组件中定义了一个Counter函数组件,返回时用被传入connect返回函数,connect函数有两个参数,第一个是我们的state仓库,第二个就是action,用以更改state仓库。在react中使用redux流程就如上述,虽然目前有点繁琐,那是因为代码少,一旦项目复杂起来,redux状态共享就会变得非常的有用。

稍微复杂一点的项目,都会将store单独拆分成一个模块出来,而不是都写在index.js中,接下来在优化一下代码:

// src/store/actions/counter.actions.js
import { INCREMENT, DECREMENT } from "../const/counter.const";

export const increment = () => ({type: INCREMENT});
export const decrement = () => ({type: DECREMENT});

// src/store/const/counter.const.js
export const INCREMENT = 'increment';
export const DECREMENT = 'decrement';
// src/store/reducer/counter.reducer.js
import { INCREMENT, DECREMENT } from "../const/counter.const";

const initialState = {
  count: 0
}

const reducer = (state = initialState, action) => {
  switch(action.type) {
    case INCREMENT:
      return {
        count: state.count + 1
      }
    case DECREMENT:
      return {
        count: state.count - 1
      }
    default: 
      return state;
  }
}

export default reducer
//// src/store/index.js
import { createStore } from "redux";
import Reducer from './reducers/counter.reducer'

export const store = createStore(Reducer)

这样我们的根目录下的index.js就会非常的简洁了:

import React from 'react';
import ReactDOM from 'react-dom/client';
// import App from './App';

import { Provider } from 'react-redux';

import Counter from './components/Counter.js'
import { store } from './store'; 

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <Provider store={store}>
   <Counter/>
</Provider>
);

实际上action也可以传参的:

在action中传入参数

<button onClick={ () => increment(5)}>+</button>

定义action时接受参数

const reducer = (state = initialState, action) => {
  switch(action.type) {
    case INCREMENT:
      return {
        count: state.count + action.payload
      }
    case DECREMENT:
      return {
        count: state.count - action.payload
      }
    default: 
      return state;
  }
}
中间件开发
概念

中间件本质就是一个函数,允许我们来扩展redux程序,很大的扩展了我们对action的扩展能力。当我们增加了中间件以后,组件触发了action,首先执行中间件函数,当中间件处理完成后,才会执行reducer。

加入了中间件的redux,其工作流程是这样的

案例

现在在点击按钮加减时需要延时1s才会数值才会改变,首先编写一个中间件:

//  middleware/thunk.js
const thunkMd =  ({dispatch}) => next => action => {
  if(action.type === 'increment' || action.type === 'decrement'){
    setTimeout(() => {
      next(action)
    },1000)
  }
  // next(action)
}

export default thunkMd

然后注册这个中间件

// store/index.js
import thunk from './middleware/thunk'

export const store = createStore(Reducer,applyMiddleware(thunk))

功能已经出现了,然而这个中间件不够灵活,我们想实现一个延时中间件,不仅仅只有这个计数器案例可以使用,要足够的抽象,可以根据传入的参数来判断,如果传入的那参数是函数,就执行我们的函数,在函数中执行异步操作;不然就正常往后执行:

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

export default thunkMd

定义异步action

export const increment_async = payload => dispatch => {
  setTimeout(() => {
    dispatch(increment(payload))
  },1000)
}

然后触发action时

function Counter({count,increment,decrement,increment_async}){
  return (
    <div>
      <button onClick={ () => increment_async(5)}>+</button>
      <span>{count}</span>
      <button onClick={() => decrement(5)}>-</button>
    </div>
  )
}
常用的中间件
  • redux-thnk

    这个中间件和上述编写的中间件完全一样的,用法也很简单,和上述完全类似,首先npm install redux-thunk

    import {applyMiddleware} from 'redux'
    createStore(rootReducer,applyMiddleware(thunk))
    
    const loadPosts = () => async dispatch => {
    	const posts = await axios,get('api').then(res => res.data)
    	dispatch({type:LOADPOSTS,payload:posts})
    }
    
  • redux-saga

    redux-saga和redux -thunk一样,都是在redux工作流程中添加异步代码 ,redux-saga更加强大,它允许将异步操作从Action Creator文件中抽离出来,放在一个单独的文件中。

    首先安装下这个插件npm install redux-saga

    // store/index.js
    import createSagaMidddleware from 'redux-saga';
    import counterSaga from './sagas/counter.saga'
    
    // 创建 sagaMiddleware
    const sagaMiddleware = createSagaMidddleware();
    
    export const store = createStore(Reducer, applyMiddleware(sagaMiddleware));
    
    sagaMiddleware.run(counterSaga)
    
    //src/sagas/counter.saga.js
    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)
    }
    
    export const INCREMENT_ASYNC = 'increment_async';
    

    触发actions

    function Counter({count,increment,decrement,increment_async}){
      return (
        <div>
          <button onClick={ () => increment_async(5)}>+</button>
          <span>{count}</span>
          <button onClick={() => decrement(5)}>-</button>
        </div>
      )
    }
    

第一篇基础总结,后面写一个购物车的案例,在研究下源码手写一个redux。加油。

代码地址