一文讲透 redux中间件及其原理

536 阅读9分钟

前言~

在正式讲redux中间件之前,我们先讲讲怎么应用中间件,以及自定义中间件怎么编写,再深入
applyMiddleWare原理,看看是如何处理中间件。

应用中间件

redux这个库给我门提供了应用中间件的函数 applyMiddleWare,专门用于应用中间件函数。

  • 自定义中间件
/**
 * 自定义中间件函数,store 为 store仓库中的store
 * @param {*} store 
 * @returns 返回一个dispatch 创建函数
 */
const logger = store => {
    // 返回一个dispatch创建函数
    return (next)  => {
        console.log("logger   中间件运行了 ===", next)
        // 返回一个dispatch函数
        return (action) => {
            console.log("action   中间件运行了 ===", action)
            next(action)
        }
    }
}

创建一个 logger函数,接受一个store,该值仓库中的 store用于得到仓库中的dispatch、getState ...中的方法。返回值为一个 dispatch创建函数,该函数接收一个经过下一个 中间件 包装过后的 dispatch ,我们命名为 next,返回值为一个真正的 dispatch 函数,与仓库中件 dispatch 合并。

读到这,你可能会产生很多疑问,next参数是怎么经过下一个中间件包装的呢?具体的包装过程是怎么实现的呢?咱们移步下文~ applyMiddleWare 源码部分

  • applyMiddleWare 具体使用中间件:
import {createStore, applyMiddleware, bindActionCreators} from "redux";
const logger = store => {
    // 返回一个dispatch创建函数
    return (next)  => {
        console.log("logger    中间件运行了 ===", next)
        return (action) => {
            next(action)
        }
    }
}

const logger1 = store => {
    // 返回一个dispatch创建函数
    return (next)  => {
        console.log("logger1  dispatch中间件运行了 ===", next)
        // 返回一个dispatch函数
        return (action) => {
            console.log("logger1 action 中间件运行了 ===", action)
            next(action)
        }
    }
}
const store = applyMiddleware(logger,logger1)(createStore)(reducer);

applyMiddleWare 函数传入了两个logger、logger1自动义的中间件,然后返回了一个函数,函数执行传入了 createStore创建仓库的函数,然后又返回一个函数,执行传入了一个reducer函数。柯里化函数应用典范!!

applyMiddleWare 原理

import compose from "./compose";
/**
 * 
 * @param  {...any} middlewares 得到一个中间件数组
 * @returns 返回一个创建仓库的函数
 */
export default function ApplyMiddleware(...middlewares) {
    /**
     * 返回一个 仓库创建函数
     */
    return function (createStore) {
        /**
         * 返回一个接收 reducer函数
         */
        return function(reducer,defaultState) {
          let store =  createStore(reducer,defaultState);
          let dispatch = () => { throw new Error("目前还不能使用dispatch") };
          let middleWareApi = {
              dispatch: (action) => dispatch(action), // dispatch 指向一个函数,然后调用新的dispatch,联动变化,保持引用地址一致。
              getState: store.getState
          }
            //根据中间件数组,得到一个dispatch创建函数的数组
         let dispatchCreators =  middlewares.map( middleWare => middleWare(middleWareApi))
         dispatch = compose(...dispatchCreators)(store.dispatch);
         console.log("----dispatch-----", dispatch)
         return {
             ...store,
             dispatch,
         }
        }
    }
}

ApplyMiddleware 返回一个接收创建仓库的函数,用于接收仓库的创建函数,执行该函数,返回一个新的函数,用于接收 reducer、defaultState, reducer创建函数和仓库默认值。在该函数内部,通过createStore函数创建了一个仓库,创建了一个初始化 dispatch 函数。

  • 细讲 middleWareApi 该 api 是作为给每个中间件 store 传递 dispatch、getState两个方法,你可能疑惑,为什么dispatch 属性不直接赋值 store.dispatch,而是要创新创建一个函数?

该操作是为了解决thunk处理副作用中间件的问题。当我们使用thunk的时候, 可以在action进行副作用处理,action返回可以是一个 函数thunk中间件在处理的时候,判断 action为一个函数,直接 把 action 当作函数执行,然后把 最新包装的dispatch传递给 action这个函数。 如果不使用函数,而是使用 store.dispatch,则当dispatch经过包装更新后,middleWareApi 的 dispatch的引用还是原来 store.dispatch的引用。使用函数就是为了 在每次dispatch 时都能更新到最新的dispatch,去触发action。

//根据中间件数组,得到一个dispatch创建函数的数组
let dispatchCreators =  middlewares.map( middleWare => middleWare(middleWareApi))

执行每个中间件函数, 得到一个每个中间件函数,创建dispatch函数的数组。

  • compose 从后向前组包装dispatch函数,该函数接收 dispatch数组,把多个 dispatch 创建函数执行 返回一个单一dispatch函数,compose返回一个函数,接收 store.dispatch 参数。 compose 函数的具体实现:
export default function compose(...dispatchCreators) {
    // if (dispatchCreators.length === 0) {
    //     return args => args; //如果没有要组合的函数,则返回的函数原封不动的返回参数
    // }
    // else if (dispatchCreators.length === 1) {
    //     //要组合的函数只有一个
    //     return dispatchCreators[0];
    // }
    // return dispatchCreators.reduce((a,b) => (...regs) => a(b(...regs)))
    return function(...args) {
        let lastReturn = null;
       for(let i = dispatchCreators.length - 1; i >= 0 ; i --) {
           const dispatchCreate = dispatchCreators[i]
            if(i === dispatchCreators.length - 1) { // 表示为最后一个中间件的dispatch创建函数
               lastReturn = dispatchCreate(...args);    // 最后一个 中间件把 dispatch函数返回 交给 前面一个中间件
            }else {
                lastReturn = dispatchCreate(lastReturn);
            }
       }
        return lastReturn
    }
}

compose 函数接收一个 dispatch创建函数的数组,返回一个函数,接收dispatch参数,从后往前遍历 dispatch创建函数,把仓库原始的 dispatch函数,传递到dispatch创建函数中, lastReturn 变量接收一个新的 dispatch 函数,把该函数当作参数,移交给下一个 中间件的 dispatch创建函数。

我们回到 logger 自定义中间件


const logger = store => {
    // 返回一个dispatch创建函数
    return (next)  => {
        console.log("logger   中间件运行了 ===", next)
        // 返回一个dispatch函数
        return (action) => {
            console.log("action   中间件运行了 ===", action)
            next(action)
        }
    }
}

// return dispatchCreators.reduce((a,b) => (...regs) => a(b(...regs)))

如果 next 不执行,则后面的中间件不会执行。

为什么呢?

我们再来细看 dispatch 组合函数的过程--- dispatchCreators.reduce((a,b) => (...regs) => a(b(...regs))), 对与 applyMiddleWare 来说, a() 就相当于 第一个logger 中间件执行,然后传入 b(...regs), 就相当于是 loggeer中的 next, 从中间件本质来说就是 logger1 这个中间件,所以 next不去执行的化,logger后的中间件 都没有办法去执行.

我想上面一段描述,是大家都不太清楚的,以上都是个人目前对于 applyMiddleWare的理解,还希望大家能指出其中的不足 ~ ~ 感谢

redux-thunk

redux-thunk中间件的作用是 我们可以action中进行副作用处理,在不使用 thunk时,action只能时一个平面对象。

在action 中进行副作用处理:

/**
 * 获取用户数据的副作用函数
 */
export function fetchUsers() {
    // dispatch 是thunk中间件传递过来的参数,还有 getState、extra 额外的参数
    return async function (dispatch) {
        dispatch(createSetIsloading(true));  // 获取用户前, 显示正在加载数据
        const users =  await getAllStudents();   // 发送ajax请求获取学生数据

        // 获取设置学生用户的action
        const action = createSetUser(users);
        dispatch(action);   // 通过dispatch 向仓库分发数据
        dispatch(createSetIsloading(false)) // 关闭正在加载数据
    }
}

action 创建函数返回一个函数,该函数交给thunk中间件进行处理,在thunk内部判断如果是函数,则直接执行该函数,把仓库原始的 dispatch、getState当参数传给action函数。 以上就是在 thunk中进行副作用处理,在thunk内部是怎么实现的呢?

  • thunk 源码
function createThunkMiddleware(extra) {
    // 返回一个 redux 中间件函数
    return store => next => action => {
        if(typeof action === 'function') {
            action(store.dispatch, store.getState, extra);
        }else {
            next(action);
        }
    }
}

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

返回一个中间件函数,判断 action 的类型,如果是函数就直接调用该函数,不是则移交给下一个中间件进行处理。

redux-promise

redux-promise 允许action是一个promise 类型,如果action 是一个 promise,则会等待 promise完成, 将resolve的结果,作为action,去触发 reducer 改变仓库中的数据。

  • action为 promise
/**
 * 获取用户数据的副作用函数
 */
 export function fetchUsers() {
     return new Promise((resolve) => {
            const users =  getAllStudents();   // 发送ajax请求获取学生数据
             // 获取设置学生用户的action
            const action =  createSetUser(users);
            resolve(action)
     })
}
  • payload 为promise 如果action 不是一个promise, 则判断action中的 payload是不是promise,如果是,等待promise完成,然后将.then resolve 中的返回值 作为action的payload
/**
 * 获取用户数据的副作用函数
 */
 export function fetchUsers() {
    return {
        type: SETUSERS,
        payload: getAllStudents().then( res => res)
    }
}

redux-saga

使用saga时,在初开始的时候会启动一个 saga任务, saga任务本质:是一个生成器函数,saga会保存生成器函数创建的生成器,在saga内部控制该生成器的运行。

saga为任务提供大量的功能以供使用, 这些任务都是以指令的形式出现而且出现在yield的位置,因此可以被saga中间件控制它的执行。

本质,通过任务影响 action

saga本质上是运行一个saga任务saga任务是一个生成器函数,saga的功能都是以指令的形式出现,被放在 yield 关键字后面,因此,被sage中间件控制执行。

图解:

image.png

注意:

指令前面必须使用 yield

在saga任务中,如果yield一个普通数据,saga不做任何处理,会将数据传递给yield表达式(把数据放到 next方法中),在saga中 yield一个普通数据没意义。

saga 需要你在yield后,执行一些 saga指令, saga中间件会对不同的指令进行不同的特殊处理,控制任务流程。

saga指令本质上就是函数,函数调用后,会返回一个指令对象,saga会接收该指令对象,进行不同的处理。

  • take指令:【阻塞】会阻塞saga任务不完成,监听某个action,如果action被dispatch了,该指令完成,则会进行下一步处理,take指令只监听一次,yield得到的是完整得 action对象。
   let result  = yield take(actionTypes.asyncIncrease);
   // 监控 asyncIncrease 被dispatch触发后,触发指令完成
  • all指令:【阻塞】该函数传入一个数组,数组成员都是生成器all指令会等待所有的生成器全部完成后才进行下一步处理
let result = yield all([getStudent(), studentTast()])
  • takeEvery指令:不断监听某个action,某个action被dispatch后,运行一个函数。takeEvery永远不会结束当前的生成器。
function* () {
    yield takeEvery(actionTypes.fechStudent, fetchStudent); 
    console.log("正在监听action")
}
  • delay指令:【阻塞】指定延迟的毫秒数
  • put指令:相当于dispatch一个action,用于重新触发一个action,
  • call指令:【可能阻塞】调用函数,如果调用的是一个promise,则会等待完成
    • 如果需要指定this指向,则 call(['this指向', 调用的函数], '后续参数...')
    • call({context:"", fn: 调用的函数})
  • apply指令:调用函数(通常是异步的),
    • 绑定指定this,apply('this指向','调用函数','[参数]')

-select指令:用于得到当前仓库中的数据

const state = yield select()
返回指定仓库中的数据
const state = yield select( store => store.student.condition)
  • cps指令: 【可能阻塞】用于调用那些传统回调模式的异步的callback

重点 saga中,当saga yield之后得到的结果是一个Promise对象时,他会自动等待 Promise 完成,然后把resolve的值传作为 下一次next调用的参数。 如果Promise对象被拒绝(执行的是reject回调),saga会使用 generator.throw 抛出一个error