Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

一、redux-devtools

我们之前讲过,redux可以方便的让我们对状态进行跟踪和调试,那么如何做到呢?

  • redux官网为我们提供了redux-devtools的工具;
  • 利用这个工具,我们可以知道每次状态是如何被修改的,修改前后的状态变化等等;

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

安装该工具需要两步:

  1. 第一步:在对应的浏览器中安装相关的插件(比如Chrome浏览器扩展商店中搜索Redux DevTools即可,其他方法可以参考GitHub);
  2. 第二步:在redux中继承devtools的中间件;

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
index.js:

import {createStore, applyMiddleware, compose} from 'redux'
import reducer from "./reducer.js";
import thunkMiddleware from 'redux-thunk'

// composeEnhancers函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;

// 通过applyMiddleware来结合多个Middleware,返回一个storeEnhancer
const storeEnhancer = applyMiddleware(thunkMiddleware)
const store = createStore(reducer, composeEnhancers(storeEnhancer))

export default store;

二、generator

saga中间件使用了ES6的generator语法,所以我们有必须简单讲解一下:
注意:我这里并没有列出generator的所有用法,事实上它的用法非常的灵活,大家可以自行去学习一下。

我们按照如下步骤演示一下生成器的使用过程:
在JavaScript中编写一个普通的函数,进行调用会立即拿到这个函数的返回结果。

如果我们将这个函数编写成一个生成器函数。调用iterator的next函数,会销毁一次迭代器,并且返回一个yield的结果。
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

研究一下foo生成器函数代码的执行顺序
generator和promise一起使用:
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

   // generator 和 Promise一起使用
    function* bar() {
        console.log(111)
        const result = yield new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve('Hello Generator')
            }, 3000)
        })
        console.log(result)
    }

    const it = bar()
    // console.log(it.next().value); // Promise对象
    it.next().value.then((res) => {
        it.next(res)
    })

三、redux-saga的使用

redux-saga是另一个比较常用在redux发送异步请求的中间件,它的使用更加的灵活。

Redux-saga的使用步骤如下

  1. 安装redux-saga
    yarn add redux-saga
  2. 集成redux-saga中间件
    导入创建中间件的函数;
    通过创建中间件的函数,创建中间件,并且放到applyMiddleware函数中;
    启动中间件的监听过程,并且传入要监听的saga;

store/index.js:

import {createStore, applyMiddleware, compose} from 'redux'
import reducer from "./reducer.js";
import thunkMiddleware from 'redux-thunk'
import createSagaMiddleware from 'redux-saga'
import saga from './saga'

// composeEnhancers函数
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({trace: true}) || compose;

// 通过applyMiddleware来结合多个Middleware,返回一个storeEnhancer
// 1.引入thunkMiddleware中间件(代码在上面)
// 2.创建sagaMiddleware中间件
const sagaMiddleware = createSagaMiddleware()

const storeEnhancer = applyMiddleware(thunkMiddleware, sagaMiddleware)
const store = createStore(reducer, composeEnhancers(storeEnhancer))

sagaMiddleware.run(saga) // saga为生成器函数

export default store;

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
3. saga.js文件的编写

takeEvery:可以传入多个监听的actionType,每一个都可以被执行(对应有一个takeLatest,会取消前面的)

put:在saga中派发action不再是通过dispatch,而是通过put;

all:可以在yield的时候put多个action;

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
store/saga.js:

import {takeEvery, put, all, takeLatest} from 'redux-saga/effects'
import axios from "axios";
import {FETCH_HOME_MULTIDATA} from "./constants";
import {changeBannersAction,changeRecommendsAction} from "./actionCreator";
function* fetchHomeMultidata(action) {
    const res = yield axios.get('http://123.207.32.32:8000/home/multidata')
    const banners = res.data.data.banner.list
    const recommends = res.data.data.recommend.list
    // yield put(changeBannersAction(banners))
    // yield put(changeRecommendsAction(recommends))
    yield all([
        yield put(changeBannersAction(banners)),
        yield put(changeRecommendsAction(recommends))
    ])
}
function* mySaga() {
    // takeEvery: 每个action都会被执行
    // yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultidata)
    // takeLatest:一次只能监听一个对应的action
    // yield takeLatest(FETCH_HOME_MULTIDATA, fetchHomeMultidata)
    yield all([
        yield takeEvery(FETCH_HOME_MULTIDATA, fetchHomeMultidata)
        // yield takeLatest(FETCH_HOME_MULTIDATA, fetchHomeMultidata)
    ])
}
export default mySaga

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
home.js:

import React, {PureComponent} from 'react';
import {connect} from "react-redux";
import {
    addAction,
    fetchHomeMultidataAction
}from "../store/actionCreator";

class Home extends PureComponent {
    componentDidMount() {
        this.props.getHomeMultidata()
    }

    render() {
        return (
            <div>
                <h1>Home</h1>
                <h3>当前计数:{this.props.counter}</h3>
                <button onClick={e => this.props.increment()}>+1</button>
                <button onClick={e => this.props.addNumber(5)}>+5</button>
            </div>
        );
    }
}
const mapStateToProps = state => ({
    counter: state.counter
})
const mapDispatchToProps = dispatch => ({
    increment() {
        dispatch(addAction(1));
    },
    addNumber(num) {
        dispatch(addAction(num));
    },
    getHomeMultidata() {
        dispatch(fetchHomeMultidataAction)
    }
})
export default connect(mapStateToProps, mapDispatchToProps)(Home);

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

四、打印日志需求

前面我们已经提过,中间件的目的是在redux中插入一些自己的操作:

  • 比如我们现在有一个需求,在dispatch之前,打印一下本次的action对象,dispatch完成之后可以打印一下最新的store
    state;
  • 也就是我们需要将对应的代码插入到redux的某部分,让之后所有的dispatch都可以包含这样的操作;

如果没有中间件,我们是否可以实现类似的代码呢? 可以在派发的前后进行相关的打印。
但是这种方式缺陷非常明显:

  • 首先,每一次的dispatch操作,我们都需要在前面加上这样的逻辑代码;
  • 其次,存在大量重复的代码,会非常麻烦和臃肿;

是否有一种更优雅的方式来处理这样的相同逻辑呢?

  • 我们可以将代码封装到一个独立的函数中

但是这样的代码有一个非常大的缺陷:

  • 调用者(使用者)在使用我的dispatch时,必须使用我另外封装的一个函数dispatchAndLog;
  • 显然,对于调用者来说,很难记住这样的API,更加习惯的方式是直接调用dispatch;

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

修改dispatch

事实上,我们可以利用一个hack一点的技术:Monkey Patching,利用它可以修改原有的程序逻辑;
我们对代码进行如下的修改:

  • 这样就意味着我们已经直接修改了dispatch的调用过程;
  • 在调用dispatch的过程中,真正调用的函数其实是dispatchAndLog;

当然,我们可以将它封装到一个模块中,只要调用这个模块中的函数,就可以对store进行这样的处理:
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

thunk需求

redux-thunk的作用:
我们知道redux中利用一个中间件redux-thunk可以让我们的dispatch不再只是处理对象,并且可以处理函数;那么redux-thunk中的基本实现过程是怎么样的呢?事实上非常的简单。

我们来看下面的代码:
我们又对dispatch进行转换,这个dispatch会判断传入的
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

合并中间件

单个调用某个函数来合并中间件并不是特别的方便,我们可以封装一个函数来实现所有的中间件合并:
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
我们来理解一下上面操作之后,代码的流程:
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
当然,真实的中间件实现起来会更加的灵活,这里我们仅仅做一个抛砖引玉,有兴趣可以参考redux合并中间件的源码流程。

五、Reducer代码拆分

我们先来理解一下,为什么这个函数叫reducer?
我们来看一下目前我们的reducer:

  • 当前这个reducer既有处理counter的代码,又有处理home页面的数据;
  • 后续counter相关的状态或home相关的状态会进一步变得更加复杂;
  • 我们也会继续添加其他的相关状态,比如购物车、分类、歌单等等;
  • 如果将所有的状态都放到一个reducer中进行管理,随着项目的日趋庞大,必然会造成代码臃肿、难以维护。

因此,我们可以对reducer进行拆分:

  • 我们先抽取一个对counter处理的reducer;
  • 再抽取一个对home处理的reducer;
  • 将它们合并起来;

六、Reducer文件拆分

目前我们已经将不同的状态处理拆分到不同的reducer中,我们来思考:

  • 虽然已经放到不同的函数了,但是这些函数的处理依然是在同一个文件中,代码非常的混乱;
  • 另外关于reducer中用到的constant、action等我们也依然是在同一个文件中;
    Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
    Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
    Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分

七、combineReducers函数

目前我们合并的方式是通过每次调用reducer函数自己来返回一个新的对象。
事实上,redux给我们提供了一个combineReducers函数可以方便的让我们对多个reducer进行合并:
Redux学习(三)——redux-saga的使用、编写中间件函数、Reducer文件拆分
那么combineReducers是如何实现的呢?
事实上,它也是讲我们传入的reducers合并到一个对象中,最终返回一个combination的函数(相当于我们之前的reducer函数了);

在执行combination函数的过程中,它会通过判断前后返回的数据是否相同来决定返回之前的state还是新的state; 新的state会触发订阅者发生对应的刷新,而旧的state可以有效的组织订阅者发生刷新;

上一篇:Redux笔记合集


下一篇:如何在react中使用redux