React 学习之常用 Redux Middleware

789 阅读20分钟

虽然在有些场景下需要我们自己去书写中间件来实现某些功能,但活跃的社区已经出了一些比较好的中间件了,所以,虽然我们自己知道了中间件的书写原理可以自己来封装实现,但有时候我们也得为了方便(人家的中间件功能强啊 / 懒)需要去使用他人已经写好的中间件,下面就介绍几个常用的第三方中间件

redux-logger

在上一篇文章 (React 学习之 Redux 中间件) 中,我们已经借助 [打印日志] 的需求点来自己书写中间件了,而且已经完成了所需的日志记载功能。但是这个功能不需要我们自己每次开发一个项目都来自己写一遍 (或者自己去写个工具库托管到 npm 库中),因为 redux-logger 已经帮我们实现了,而且我们只需在开发阶段使用且它功能更多、日志样式更好看,所以我们直接使用它 (它的实现原理与简单的实现代码可见上一篇文章) 即可:

import {createStore, applyMiddleware} from 'redux'
import logger from 'redux-logger'
import reducer from '@/store/combine-reducer'

const store = createStore(
    reducer,
    applyMiddleware(logger) /* 直接应用中间件即可 */
)

export default store

更多使用就去看 官方 API 说明

注意: 若与 redux-thunk 或其他可以处理副作用的中间件库连用时,需要将 logger 中间件作为 applyMiddleware 的最后一个参数使用,否则会由于 action 的原因导致它无法生效 (具体原因可见下面的 redux-thunk 原理)

redux-thunk

之前我们知道,在 redux 中是不能存在副作用操作的 (比如请求数据或其他异步操作等),而且分发的 action 必须得是一个普通的包含 type 属性的平面对象

// 所以在之前,我们请求到数据后,再分发 action 去更新 redux 中数据
// 操作只能这样处理 (写法上仅作示例,项目的实际写法可能不同)
import {getAllUsers} from '@/service/users'
import store from '@/store'
import {createSetUsersAction} from '@/store/action'

getAllUsers().then(res => {
    // res 为请求得到的 user[]
    // 数据请求成功,再去分发 action
    store.dispatch(createSetUsersAction(res))
})

// @/store/action.js ➡️ action 创建函数(返回 action 平面对象)
const SETUSERS = Symbol('setUsers')
export const createSetUsersAction = (payload) => ({
    type: SETUSERS,
    payload
})

而当我们使用 thunk 之后,我们就可以将副作用操作放到 action 创建函数中处理了,action 创建函数就可以返回一个副作用操作的函数了

redux-thunk 用法:

import {createStore, applyMiddleware} from 'redux'
import ReduxThunk from 'redux-thunk'
import logger from 'redux-logger'
import reducer from '@/store/combine-reducer'

// 我们需要手动附加的额外参数 (any 类型)
// ReduxThunk.withExtraArgument(extraArg)

// 应用中间件
const store = createStore(
    reducer,
    applyMiddleware(
        ReduxThunk, /* 替换为 ReduxThunk.withExtraArgument(extraArg) */
        logger
    )
)

// @/store/action.js ➡️ action 创建函数(返回副作用操作函数)
export const createSetUsersAction = () => {
    // thunk 会将 dispatch 函数,getState 方法和
    // 我们需要附加的一个额外参数传递进来 (一般无用)
    return async (dispatch, getState, extraArg) => {
        // 比如这里加一个分发 loading 状态的 action ➡️ true
        const res = await getAllUsers()
        dispatch(setUsersAction(res)) // 分发 设置 users 的 action
        // 数据加载成功,分发 loading 状态的 action ➡️ false
    }
}

// 使用
import {createSetUsersAction} from '@/store/action'
import store from '@/store'

store.dispatch(createSetUsersAction()) // 请求数据并设置数据

redux-thunk 原理

由上可知,thunk 允许 action 是一个带有副作用的函数,当分发的 action 是一个函数时,thunk 中间件会阻止 action 继续向后移交,而是直接调用此函数;若是普通 action 平面对象,则往后移交

理解了它的实现思路之后,就会发现它的代码实现就很简单了:

// 创建 redux-thunk.js
const createThunkMiddleware = extraArg => {
    // 返回 thunk 中间件
    return store => next => action => {
        if (typeof action === 'function') {
            return action(store.dispatch, store.getState, extraArg)
        }
        return next(action)
    }
}

const thunk = createThunkMiddleware()
thunk.withExtraArgument = createThunkMiddleware
export default thunk

redux-promise

上面的 redux-thunk 中间件允许我们分发的 action 是一个具有副作用的函数,那么我们从 redux-promise 的命名上就可以知道它可以让我们分发的 action 是一个 Promise 对象

redux-promise 用法:

import {createStore, applyMiddleware} from 'redux'
import ReduxPromise from 'redux-promise'
import logger from 'redux-logger'
import reducer from '@/store/combine-reducer'

// 应用中间件
const store = createStore(
    reducer,
    applyMiddleware(
        ReduxPromise,
        logger
    )
)

// store/action.js
// 或者使用 async 函数(本质是 Promise 语法糖)
// 方式 1: 分发的 action 本身就是一个 Promise 对象
export const createSetUsersAction = () => {
    return new Promise((resolve, reject) => {
        // 模拟 3s 后获取数据分发 action
        setTimeout(() => {
            resolve({
                type: actionTypes.setUsersAction,
                payload: 'any data...'
            })
        }, 3000)
    })
}
// 方式 2: 分发的 action 是一个普通对象,但 payload 是一个 Promise 对象
export const createSetUsersAction = () => {
    return {
        type: actionTypes.setUsersAction,
        payload: new Promise(resolve => {
            resolve(‘any data...’)
        })
    }
}

由上例可知,promise 类型的 action 通过 resolve 函数 来分发 action

而且仅在 action.payload 为 Promise 对象的情况下,它的 rejected 的结果才会做处理 (将错误结果作为 payload 分发 action)

redux-promise 原理

promise 中间件主要做了这么几步处理:

  1. 判断 action 是不是标准的 Flux Action 对象 (普通平面对象;必拥有 type 属性名,且 type 类型为 String;剩余可选属性名 ["payload", "error", "meta"]) (借助 isFSA(action))

  2. 若不是,则判断 action 是不是 Promise 对象 (借助 isPromise(action)),若 action 是 Promise 对象,则 action.then(),resolved 的结果作为 dispatch 函数分发的 action;若 action 不是 Promise 对象,则直接调用下一个中间件的 dispatch

  3. 若 action 是标准的 Flux Action 对象,则判断 action.payload 是不是 Promise 对象 (借助 isPromise(action.payload));与第二步基本类似,不过会对 rejected 的结果进行分发 action 处理 (payload 为 error 对象)

代码实现如下:

// @/redux-promise/index.js
import {isFSA} from 'flux-standard-action'
import isPromise from 'is-promise'

const promiseMiddleware = ({dispatch}) => next => action => {
    /**
    * 判断是否是标准的 Flux Action
    */
    if (!isFSA) {
        /**
        * 判断 action 是不是 promise
        * 是,则处理 .then 返回的结果
        * 否,则向后移交
        */
        return isPromise(action) ? action.then(dispatch) : next(action)
    }
    // 是标准的 Flux Action
    /**
    * 判断 action.payload 是不是 Promise 对象
    * 1. 是,则调用 payload.then 分发 action
    *    且对 rejected 的结果也分发 action 处理
    *
    * 2. 否,则向后移交
    */
    return isPromise(action.payload) ?
        action.payload
          .then(payload => dispatch({ ...action, payload }))
          .catch(error => {
              dispatch({ ...action, payload: error })
              return Promise.reject(error)
          })
        : next(action)
}

export default promiseMiddleware

redux-saga

上面介绍的 redux-thunkredux-promise 都会导致 action 或 action 创建函数不再纯粹,但它与 redux 本身的纯净性初衷其实是相背离的;而 redux-saga 就将解决这样的问题,它不仅可以保持 action 的纯净,而且可以使用 模块化 的方式解决副作用操作,而且功能强大

redux-saga 中间件是建立在 ES6 的 生成器 的基础上的,而要理解生成器,又得先理解迭代器与可迭代协议,所以我们依次来复习一下吧~

移步 ➡️ JavaScript 之迭代器与生成器 🤓

ReduxSaga 的中文文档是 ➡️ Redux Saga 中文文档

saga 任务

由于 saga 不要求 action 去做出改变,那么在应用中间件时可以放在任意位置,而不像 redux-thunk 那样需要在中间件列表首部应用

使用 saga 中间件,在最开始的时候,需要使用该中间件会启动一个 saga 任务(本质就是一个生成器函数),saga 任务提供了大量功能以供使用;这些功能是以 指令 的形式出现的,而且出现的位置是 yield 的位置,因此它的执行可以在外部被 saga 中间件控制

使用示例:

import {createStore, applyMiddleware} from 'redux'
import createSagaMiddleware from 'redux-saga'
import logger from 'redux-logger'
import reducer from './reducer'
import sagaTask from './saga' // saga 任务(一个生成器函数)

// 创建一个 saga 中间件
const sagaMid = createSagaMiddleware()

// 创建仓库
const store = createStore(reducer, applyMiddleware(sagaMid, logger))

// 启动 saga 任务
sagaMid.run(sagaTask)

export default store

saga 任务 (一般单独做一个文件):

// @/store/saga/index.js
export default function* sagaTask() {
    // 启动 saga 任务时就会打印
    console.log('saga 任务运行了')
    const res = yield 2 // yield 之后的语句是随意写的话,saga 中间件不会处理
    // 启动 saga 任务时就会打印
    console.log('saga 任务运行结束', res) // 打印:'saga 任务运行结束' 2
}
// @/store/action/counter.js 计数的 action 创建函数
export const actionTypes = {
    INCREASE: Symbol('increase'),
    DECREASE: Symbol('decrease')
}
// +1
export const increase = () => ({
    type: actionTypes.INCREASE
})

// -1
export const descrese = () => ({
    type: actionTypes.DECREASE
})
// @/store/reducer/counter.js 计数的 reducer
import { actionTypes } from '@/store/action/counter'
const counter = (state = 10, { type }) => {
    switch (type) {
        case actionTypes.INCREASE:
            return state + 1
        case actionTypes.DECREASE:
            return state - 1
        default:
            return state
    }
}
export default counter

在 saga 任务中,如果 yield 语句后跟了一个普通数据 (或非 saga 指令),saga 不做任何处理,仅仅将数据作为下次 next 方法的参数,传递进 yield 表达式的执行结果 (如上面 saga 任务中 const res = yield 2 语句);saga 中间件让我们在 yield 语句后放上一些合适的 saga 指令 (saga Effects),saga 就会根据不同的指令进行不同的处理,以控制整个任务的流程

saga Effects (saga 指令)

每个指令,本质上就是一个函数,该函数调用后,会返回一个指令对象,saga 中间件接收到这个指令对象,从而进行各种处理

saga 指令前必须使用 yield 关键字,以确保该指令的返回结果被 saga 控制

take

用来监听某个 action 的触发 (【yield take 语句会阻塞】),接受一个要监听的 action 作为参数,如果匹配到该 action 的触发 (yield 阻塞语句返回的对象则为完整的 action 对象),则会进行下一步处理 (仅能监听一次该 action 的触发,若要持续监听,可结合 死循环 使用)

import { take } from 'redux-saga/effects'
import { actionTypes } from '@/store/action/counter'

export default function* () {
    // 返回的对象则为完整的 action 对象
    // const action = yield take(actionTypes.INCREASE)
    // console.log('触发 increase:', action)

    // 结合死循环,就能一直监听 increase 的触发
    while (true) {
        const action = yield take(actionTypes.INCREASE)
        console.log('触发 increase:', action)
        // 比如结合后续的 put 指令去触发真正的无副作用的 action
        // yield put(action) // reducer 处理的 action
    }
}

以上,均是同步(或非副作用)的 action 触发,分别在 reducer 中进行数据处理;那么,如果有个异步的 action 触发的情况下,action 依旧是纯粹的,我们就可以在 saga 任务中监听到异步触发的 action :

// @/store/action/counter.js
export const actionTypes = {
    // ... other types
    ASYNC_INCREASE: Symbol('async-increase')
}

export const asyncIncrease = () => ({
    type: actionTypes.ASYNC_INCREASE
})

// @/store/saga/counter.js
import { take } from 'redux-saga/effects'
import { actionTypes } from '@/store/action/counter.js'
export default function* () {
    const action = yield take(actionTypes.ASYNC_INCREASE)
    console.log('异步的 increase 触发:', action)
}

all

该函数接受一个 数组参数,数组中放入 生成器 (【yield all 语句会阻塞】),saga 中间件会等待所有的生成器 全部完成 后才会进一步处理,所以我们可以把各自没有关联的功能进行分模块编写 saga 任务,再通过 all Effects 进行合并 (创建的 saga 中间件通过 .run 函数传入合并后的 saga 任务即可)

// 比如:处理 counter 的 saga 任务
// @/store/saga/counter.js
import { take } from 'redux-saga/effects'
export default function* () {
    const action = yield take(actionTypes.ASYNC_INCREASE)
    console.log('异步的 increase 触发:', action)
}

// 比如:处理 时间 的 saga 任务
// @/store/saga/time.js
import { take } from 'redux-saga/effects'
export default function* () {
    const action = yield take(actionTypes.SET_COMMON_TIME)
    console.log('设置 common time 触发:', action)
}

// @/store/saga/index.js 合并所有的 saga 任务
import { all } from 'redux-saga/effects'
import counterTask from './counter'
import timeTask from './time'
export default function* () {
    // 传入的是生成器数组
    yield all([counterTask(), timeTask()])
    // 等待所有 saga 任务结束才会执行下面的代码
    console.log('saga 完成')
}

takeEvery

不断地监听某个 action (永远不会结束监听,【yield takeEvery 语句不会阻塞】),当某个 action 到达之后,运行一个函数;它接受两个参数:(要监听的 actionaction 触发时要运行的生成器函数);要监听的 action 可以是通配符 * (不过要求你书写的 action 的类型必须是 string 类型)

import { takeEvery } from 'redux-saga/effects'

// 监听函数
function* asyncIncreaseHandler() {
    // 在这里进行 action 触发后的相关处理
}

function* asyncDecreaseHandler() {
    // 在这里进行 action 触发后的相关处理
}

export default function* () {
    // 异步增加
    yield takeEvery(actionTypes.ASYNC_INCREASE, asyncIncreaseHandler)
    // 异步减少
    yield takeEvery(actionTypes.ASYNC_DECREASE, asyncDecreaseHandler)
    console.log('正在监听 asyncIncrease') // 会直接打印
}

到这里,你会发现:'正在监听 asyncIncrease' 会立即打印

综上,saga 中间件的书写结构一般为:

// 比如,目录结构如下 (当然,怎么划分目录结构你自己定):
| store
|--- saga
|------ counter.js
|------ time.js
|------ index.js // 合并 saga 任务
|--- action
|------ counter.js
|------ time.js
|------ index.js // 把所有的 action 类型通过此文件导出
|--- reducer
|------ counter.js
|------ time.js
|------ index.js // 把所有的 reducer 合并为一个 reducer 导出
|--- index.js // 创建仓库、应用中间件...并导出
  1. 创建仓库,应用中间件

    // @/store/index.js
    import {createStore, applyMiddleware} from 'redux'
    import reducer from './reducer'
    import createSagaMiddleware from 'redux-saga'
    import logger from 'redux-logger'
    import rootSagaTask from './saga'
    
    const sagaMiddleware = createSagaMiddleware()
    const store = createStore(
        reducer,
        applyMiddleware(sagaMiddleware, logger)
    )
    sagaMiddleware.run(rootSagaTask)
    
    export default store
    
  2. saga 根任务,合并所有分模块书写的 saga 任务

    // @/store/saga/index.js
    import { all } from 'redux-saga/effects'
    import counterTask from './counter'
    import timeTask from './time'
    
    export default function* () {
        yield all([counterTask(), timeTask()])
        console.log('saga 任务完成 over~')
    }
    
  3. saga 模块子任务 (以 counter 为例)

    // @/store/saga/counter.js
    import { takeEvery } from 'redux-saga/effects'
    import { actionTypes } from '../action/counter'
    
    function* asyncIncreaseHandler() {
        // 在这里进行 action 触发后的相关处理
        yield ...
    }
    
    function* asyncDecreaseHandler() {
        // 在这里进行 action 触发后的相关处理
        yield ...
    }
    
    export default function* () {
        yield takeEvery(actionTypes.ASYNC_INCREASE, asyncIncreaseHandler)
        yield takeEvery(actionTypes.ASYNC_DECREASE, asyncDecreaseHandler)
    }
    

delay

指定延迟的毫秒数(【yield delay 语句会阻塞】),它可以接受两个参数:delayTime(ms)[return value] (可选),分别表示延迟执行的毫秒数及 yield delay 的执行返回结果 (默认返回 true)

比如上面的 counter 子任务例子:

import { takeEvery, delay } from 'redux-saga/effects'
import { actionTypes } from '../action/counter'

function* asyncIncreaseHandler() {
    /**
    * 不能使用 setTimeout 函数,是因为 setTimeout 传递的
    * 回调函数是不能被 saga 控制的,在这个回调函数中是不能
    * 使用其他 saga 指令的,从而会使程序就变得不可控
    */
    // 比如处理延迟增加【测试用 delay 指令】
    const res = yield delay(2000, 'return value')
    console.log('asyncIncreaseHandler ', res) // 'return value'
}

export default function* () {
    yield takeEvery(actionTypes.ASYNC_INCREASE, asyncIncreaseHandler)
    // yield takeEvery(actionTypes.ASYNC_DECREASE, asyncDecreaseHandler)
}

put

用于重新触发 action (【yield put 语句会阻塞】),相当于 dispatch(action),接受一个参数为 action 平面对象

比如上面的 counter 子任务例子,我们在 reducer 中并未处理副作用操作去异步增加,我们就可以在 action 监听函数中进行副作用处理,等待处理完成后去触发正常的 action:

import { takeEvery, delay, put } from 'redux-saga/effects'
import { actionTypes, increase } from '../action/counter'

function* asyncIncreaseHandler() {
    yield delay(2000) // 比如处理延迟增加【测试用 delay 指令】
    console.log('after delay 2s...')
    yield put(increase()) // 2s 后触发增加的 action
}

export default function* () {
    yield takeEvery(actionTypes.ASYNC_INCREASE, asyncIncreaseHandler)
}

call

用于副作用函数调用(通常是异步函数调用,【yield call 语句可能会阻塞】,若是 Promise 则会 阻塞 代码执行),可以接受 n 个参数,第一个参数即是需要进行副作用的函数,若这个副作用函数需要参数,往后依次传入即可

如果副作用函数需要绑定 this:

  • call 指令 的第一个参数要是一个包含两个参数的 数组:数组的第一个参数为需要绑定的 this 指向,第二个参数则为副作用函数;后面参数一样

  • call 指令 的第一个参数要是一个 对象context 属性 为 this 指向,fn 属性 为副作用函数;后面参数一样

saga 任务中,yield 碰到 Promise 时,会等待 Promise 的结果,然后将 resolved 的结果 作为值传递到下一次 next;若是 rejected 的结果 将直接报错(generator.throw 抛出一个错误)。所以在这类处理中我们一般使用 try...catch... 进行错误拦截处理,示例代码如下:

import {takeEvery, call, put} from 'redux-saga/effects'
// 比如在网络请求获取数据时
function* watchFetchUsers() {
    yield put(setIsLoading(true)) // 全局正在加载
    /**
    * yield Promise 的语句会等待完成,再将结果传递给 resp
    * 比如获取 users 数据
    */
    try {
        // const resp = yield fetchAllUsers()
        // yield 直接跟 Promise 对象虽然可以正常运行
        // 但推荐使用统一的指令格式(也方便后续单元测试等阶段)
        const resp = yield call(fetchAllUsers)
        console.log(resp) // 打印请求返回的结果
        
        // 1. 如果 fetchAllUsers 需要绑定 this
        // const resp = yield call([{ desc: 'this 指向绑定' }, fetchAllUsers])
        
        // 2. 如果 fetchAllUsers 需要绑定 this
        // const resp = yield call({
        //     context: 'this 指向绑定',
        //     fn: fetchAllUsers
        // })
        
        yield put(setAllUsers(resp)) // 触发设置所有用户的 action
    } catch(err) {
        // 打印错误或使用 消息提示组件 进行警告提示
        // err 为 reject 的内容或 fetchAllUsers 方法的报错信息
        console.error(err)
    } finally {
        yield put(setIsLoading(false))
    }
}

export default function* () {
    yield takeEvery(actionTypes.GET_ALL_USERS, watchFetchUsers)
}

apply

功能与 call 指令 一样 (【yield apply 语句可能会阻塞】),也是用于调用副作用函数的,只是传参不同,它最多只需要三个参数:第一个参数是需要绑定的 this 指向 (不需要则传递 null),第二个参数则是副作用函数,第三个参数则是副作用函数所需要的参数(以数组形式传递)

import {takeEvery, apply, put} from 'redux-saga/effects'
// 比如在网络请求获取数据时
function* watchFetchUsers() {
    yield put(setIsLoading(true)) // 全局正在加载
    try {
        const resp = yield apply(null, fetchAllUsers)
        
        yield put(setAllUsers(resp)) // 触发设置所有用户的 action
    } catch(err) {
        console.error(err)
    } finally {
        yield put(setIsLoading(false))
    }
}

export default function* () {
    yield takeEvery(actionTypes.GET_ALL_USERS, watchFetchUsers)
}

select

用于取出当前仓库中的数据 (【yield select 语句不会阻塞】),它可以接受一个参数(函数),用于筛选返回的仓库数据,这个函数的返回值则为拿到的状态值(若 select 指令不传递参数,则返回整个仓库对象)

比如,以当前仓库中的分页信息查询用户信息:

import {takeEvery, select, call, put} from 'redux-saga/effects'
// 比如在网络请求获取数据时
function* watchFetchUsers() {
    yield put(setIsLoading(true)) // 全局正在加载
    try {
        // 拿到当前仓库中存储的状态,只获取 users 部分
        const users = yield select(state => state.users)
        const resp = yield call(fetchUsersByPage, {
            pageNo: users.pageNo,
            pageSize: users.pageSize
        })
        
        yield put(setPageUsers(resp)) // 触发设置用户分页结果的 action
    } catch(err) {
        console.error(err)
    } finally {
        yield put(setIsLoading(false))
    }
}

export default function* () {
    yield takeEvery(actionTypes.GET_PAGE_USERS, watchFetchUsers)
}

cps

用于调用那些传统回调方式的异步函数 (【yield cps 语句可能会阻塞】,取决于 callback 的调用时机),cps:Continuation Passing Style(延续传递风格)

上面的 call 和 apply 指令都非常适合返回 Promise 结果的函数,cps 则就非常适合处理 Node 风格 的函数(fn(...args, callback),这个 callback 是 (error, result) => ... 的形式;相信有写过的小伙伴都比较了解,我们要写一堆嵌套的回调函数来进行流程处理与逻辑控制)

// 比如,在 ES6 之前,没有 Promise 的情况下请求数据
// 我们模拟 3s 后成功获得数据,我们需要使用 callback 来传递
// 可以不是一个生成器函数
function mockPageUsers(callback) {
    setTimeout(() => {
        if (Math.random() > 0.5) {
            callback(new Error('出错了'), null)
            return
        }
        callback(null, {
            count: 99,
            list: [
                { id: 1, name: 'Saul', age: '25' },
                { id: 2, name: 'suressk', age: '25' }
            ]
        })
    }, 3000)
}

import {takeEvery, select, cps, put} from 'redux-saga/effects'
function* watchFetchUsers() {
    yield put(setIsLoading(true)) // 全局正在加载
    try {
        // 拿到当前仓库中存储的状态,只获取 users 部分
        const resp = yield cps(mockPageUsers)
        
        yield put(setPageUsers(resp)) // 触发设置用户分页结果的 action
    } catch(err) {
        console.error(err)
    } finally {
        yield put(setIsLoading(false))
    }
}

export default function* () {
    yield takeEvery(actionTypes.GET_PAGE_USERS, watchFetchUsers)
}

fork

用于开启一个新的任务 (【yield fork 语句不会阻塞】),它接受一个生成器函数作为参数

比如,上面在说到 takeEvery 指令时,我们使用它持续监听 action 的触发,我们也可以换用 fork 来实现一样的效果:

// @/store/saga/counter.js
import { takeEvery, fork, delay } from 'redux-saga/effects'
import { actionTypes } from '../action/counter'

function* asyncIncreaseHandler() {
    // 在这里进行 action 触发后的相关处理
    yield delay(10000) // 比如延迟10s
    console.log('fork task over~')
}

function* asyncDecreaseHandler() {
    // 在这里进行 action 触发后的相关处理
    yield ...
}

export default function* () {
    // yield takeEvery(actionTypes.ASYNC_INCREASE, asyncIncreaseHandler)
    yield fork(asyncIncreaseHandler) // 开启一个新的任务
    yield takeEvery(actionTypes.ASYNC_DECREASE, asyncDecreaseHandler)
    console.log('counter task root~')
}

上面这个例子 🌰 运行起来后的打印结果是:

'counter task root~'
// 10s 后
'fork task over~'

相当于是开了一个新的线程去处理 asyncIncreaseHandler 的任务;

redux-actions

从实际上来说,它并不是一个 redux 中间件,而仅仅是一个第三方库;用于简化 action-typesaction-creator 以及 reducer 的书写

➡️ API Reference

createAction(s)

createAction

用于创建一个 action 创建函数,简化我们书写的代码

  • 【必传】createAction 接受一个 action 类型 作为参数

  • 【可选】若需要 payload,则第二个参数为一个函数 (payloadCreator),这个函数的返回值即为要传递的 payload 值

  • 【可选】若需要 meta (附加的元数据),与第二个参数一样,接受一个函数作为第三个参数,其返回值即为 meta 数据 (一般用不到)

写法如下:

import {createAction} from 'redux-actions'

// 无 payload 的 action-creator
export const increase = createAction(actionTypes.increase)
export const decrease = createAction(actionTypes.decrease)
export const asyncIncrease = createAction(actionTypes.asyncIncrease)

// 有 payload 的 action-creator
export const add = createAction(actionTypes.add, num => num)

// 得到的结果同下:
// export const increase = () => ({
//     type: actionTypes.increase
// })
// export const decrease = () => ({
//     type: actionTypes.decrease
// })
// export const asyncIncrease = () => ({
//     type: actionTypes.asyncIncrease
// })
// 有参数
// export const add = num => ({
//     type: actionTypes.decrease,
//     payload: num
// })

那么,我们可以得知 createAction 函数 的原理如下:

function createAction(type, payloadCreator) {
    return function actionCreator() {
        if (typeof payloadCreator === 'function') {
            return {
                type,
                payload: payloadCreator()
            }
        }
        return { type }
    }
}

createActions

用于创建多个 action 创建函数,需要一个参数 (是一个对象,属性名即为 action 类型,属性值为 payloadCreator 函数 / null);它的返回值是一个属性名为 小驼峰命名 式的对象

使用表现结果与上面分开写的示例一致,写法如下:

import {createActions} from 'redux-actions'

export const {
    increase,
    descrease,
    asyncIncrease,
    add,
    fetchUsers
} = createActions({
    INCREASE: null,
    DECREASE: null,
    ASYNC_INCREASE: null,
    ADD: num => num,
    FETCH_USERS: null
    // 获取数据在 saga 中处理(如果用 saga 的话),reducer 中不用管
    // 即:后面的 handleActions 中不用处理
})

原理如下 (这里的 key 仅考虑字符串类型,未考虑 Symbol 类型的 key):

function createActions(actionCreators) {
    const res = {}
    for (const prop in actionCreators) {
        const payloadCreator = actionCreators[prop] // 属性值
        const propName = toSmallCamel(prop) // 小驼峰命名 (string)
        
        const actionCreator = (...args) => {
            if (typeof payloadCreator === 'function') {
                return {
                    type: prop,
                    payload: payloadCreator(...args)
                }
            }
            return { type: prop }
        }
        actionCreator.toString = () => prop // 为下面 handleAction(s) 服务
        
        // 生成 action 创建函数
        res[propName] = actionCreator
    }
    return res
}

// 辅助函数:分割线命名 转换为 小驼峰命名
function toSmallCamel(name) {
    return name.split('_').map((s, i) => {
        s = s.toLowerCase()
        if (i !== 0 && s.length >= 1) {
            s = s[0].toUpperCase() + s.substr(1)
        }
        return s
    }).join('')
}

handleAction(s)

handleAction

简化针对 单个 action 类型的 reducer 处理,当它匹配到对应的 action 类型后,会执行对应的函数

handleAction(type, reducer, defaultState)

  • 第一个参数 type 为 action 类型 (字符串、symbol 或 createAction(s) 返回的函数);因为 createAction(s) 返回的函数会重写 toString 方法,它会返回 action 的类型

  • 第二个参数 reducer 为 action 匹配上的处理函数,返回值则为更新后的 state 值

  • 第三个参数 defaultState 为状态默认值

比如,整个 redux 只涉及 count 值的增加,可以通过此函数简化 (而不需要去手写 switch...case... 语句)

import { handleAction } from 'redux-actions'

const reducer = handleAction(
    actionTypes.increase,
    state => state + 1,
    10
)

// 相当于我们之前 reducer 的写法:
const reducer = (state, { type }) => {
    switch (type) {
        case actionTypes.increase:
            return state + 1
        default:
            return state
    }
}

handleActions

简化 多个 action 类型的 reducer 处理

handleActions(reducerMap, defaultState)

  • reducerMap【必传】,普通对象或 Map 对象,结构为:key 值为 action 类型 (字符串、symbol 或 createAction(s) 函数返回的函数),值为 reducer 处理函数

  • defaultState【必传】,默认状态数据

import {handleActions} from 'redux-actions'

const reducer = handleActions({
    INCREASE: state => ({
        count: state.count + 1
    }),
    DECREASE: state => ({
        count: state.count - 1
    }),
    ADD: (state, { payload }) => ({
        count: state.count + payload
    })
}, {
    count: 0
})

// 或者是 Map
const reducer = handleActions(
    new Map([
        [
           INCREASE,
           state => ({
                count: state.count + 1
           })
        ],
        [
           DECREASE,
           state => ({
                count: state.count - 1
           })
        ],
        [
           ADD,
           (state, { payload }) => ({
                count: state.count + payload
           })
        ],
    ]),
    /* default state */
    {
        count: 0
    }
)

由上,我们就可以将 action 创建函数与 reducer 处理写到一起了,外面进行 reducer 合并什么的并不会有影响:

import { createActions, handleActions } from 'redux-actions'

// 得到对应的 action 创建函数
export const {
    increase,
    descrease,
    add
} = createActions({
    INCREASE: null,
    DECREASE: null,
    ADD: num => num
})

// reducer 处理
export default handleActions({
    [increase]: state => ({
        count: state.count + 1
    }),
    [decrease]: state => ({
        count: state.count - 1
    }),
    [add]: (state, { payload }) => ({
        // ...state, // 如果有其他数据
        count: state.count + payload
    })
},
{
    count: 0
})

combineActions

配合 createActionshandleActions 两个函数,用于处理多个 action-type 对应同一个 reducer 处理函数

比如,上面的 increasedescrease,之前是不用传递 payload 的,现在也需要传递 payload 的方案:

import { createActions, handleActions } from 'redux-actions'

export const {
    increase,
    descrease,
    add
} = createActions({
    INCREASE: () => 1,
    DECREASE: () => -1,
    ADD: num => num
})

export default handleActions({
    [increase]: (state, { payload }) => ({
        count: state.count + payload
    }),
    [decrease]: (state, { payload }) => ({
        count: state.count + payload
    }),
    [add]: (state, { payload }) => ({
        // ...state, // 如果有其他数据
        count: state.count + payload
    })
},
{
    count: 0
})

我们发现,下面 reducer 处理时的代码是完全重复的,我们就可以使用 combineActions 进行合并操作:

import {
    createActions,
    handleActions,
    combineActions
} from 'redux-actions'

export const {
    increase,
    descrease,
    add
} = createActions({
    INCREASE: () => 1,
    DECREASE: () => -1,
    ADD: num => num
})

// 三者完全一样的 reducer 处理合并为一个函数,会匹
// 配这三个 action 类型,触发 count 值的变更
const countChange = combineActions(increase, descrease, add)

export default handleActions({
    [countChange]: (state, { payload }) => ({
        // ...state, // 如果有其他数据
        count: state.count + payload
    })
},
{
    count: 0
})

结语

至此,Redux 的常用中间件就介绍到此,知识点及用法巨多

相比之下还是 vuex 用起来香啊~🌚