ZHANGYU.dev

October 14, 2023

Redux的中间件applyMiddleware解析

JavaScript4.9 min to read

最近在阅读redux的源码,觉得十分的精妙,但是也看出了自己对Typescript的理解还不到位

其中中间件部分的代码,第一次让我感觉到了心灵的冲击,原来代码可以这么的精巧绝伦

以前自己封装过一个简单的请求方法,中间件的实现就low的不谈了,创建一个前置中间件的数组,再创建一个后置中间件的数组,请求前遍历掉用前置数组,请求后遍历掉用后置数组,真的捞的淌口水

redux的中间件实现

先来看看源码,为了看上去直观一点,我把ts的类型给去掉了

// 组合中间件函数的工具函数// 把单参数函数从右到左嵌套调用export default function compose(...funcs) {  if (funcs.length === 0) {    // infer the argument type so it is usable in inference down the line    return arg => arg;  }  if (funcs.length === 1) {    return funcs[0];  }  return funcs.reduce((a, b) => (...args) => a(b(...args)));}export default function applyMiddleware(...middlewares) {  return createStore => (reducer, ...args) => {    // 这里接管了store的创建    const store = createStore(reducer, ...args);    // 这里做一个错误处理    // 如果在绑定中间件的时候调用dispatch会报错    let dispatch = () => {      throw new Error(        "Dispatching while constructing your middleware is not allowed. " +          "Other middleware would not be applied to this dispatch."      );    };    const middlewareAPI = {      getState: store.getState,      dispatch: (action, ...args) => dispatch(action, ...args)    };    // 将dispatch和getStore方法传入中间件,得到新的数组    const chain = middlewares.map(middleware => middleware(middlewareAPI));    // 将新的数组用compose绑定起来,再把store.dispatch传入,得到新的dispatch    dispatch = compose(...chain)(store.dispatch);    // 返回新的dispatch,这个dispatch会触发中间件    return {      ...store,      dispatch    };  };}

这个applyMiddleware函数接受不定参数的函数作为参数,返回一个新的函数,这个新的函数会接受createStore函数作为参数

使用了applyMiddleware函数后,reduxcreateStore创建store就移交给applyMiddleware函数处理了

中间件函数接受的参数是dispatchgetState函数,并且返回一个新的函数

来看看中间件函数应该怎么写

const doNothingMiddleware = middlewareApi => next => action =>  next(action);

这里的middlewareApi就是下面这段传入的dispatch函数和getState函数

const chain = middlewares.map(middleware => middleware(middlewareAPI));

这里对所有中间件传了dispatchgetState,中间件返回的新函数里,可以闭包使用这两个函数

其实中间件的实现,只用了一行代码

dispatch = compose(...chain)(store.dispatch);

重点就在于这个compose函数

compose函数

export default function compose(...funcs) {  if (funcs.length === 0) {    // infer the argument type so it is usable in inference down the line    return (arg) => arg;  }  if (funcs.length === 1) {    return funcs[0];  }  return funcs.reduce((a, b) => (...args) => a(b(...args)));}

这个函数作用简单来讲,就是把单参数函数从右到左嵌套调用

举个例子

const a = str => str + "A";const b = str => str + "B";const c = str => str + "C";const composed = compose(a, b, c); // (...args)=> a(b(c(...args))composed("args"); // => argsCBA

一步一步解析,这里有3个函数,所以reduce会遍历2次

第一次

f1 = (...args) => a(b(...args))

第二次

f2 = (...args) => f1(c(...args)) // 也就是 a(b(c(...args))

以此类推

这只是简单的一个示例,实际上需要更复杂的例子才能理解redux是如何实现中间件的

从代码理解

先实现一个简单的中间件功能

// 组合函数,没有做参数个数的判断const compose = (...funcs) =>  funcs.reduce((a, b) => (...args) => a(b(...args)));// 中间件Aconst middlewareA = next => action => {  console.log("before middlewareA");  console.log("middlewareA action =>", action);  next(action);  console.log("after middlewareA");};// 中间件Bconst middlewareB = next => action => {  console.log("before middlewareB");  console.log("middlewareB action =>", action);  next(action);  console.log("after middlewareB");};// 处理的函数const handle = () => {  console.log("处理中");  console.log("处理完毕");};// 通过compose后,具有中间件功能的函数const dispatch = compose(middlewareA, middlewareB)(handle);dispatch({ type: "study" });

结果还是符合预期的

before middlewareAmiddlewareA action => { type: 'study' }before middlewareBmiddlewareB action => { type: 'study' }处理中处理完毕after middlewareBafter middlewareA

其实我觉得比较难理解的地方,是在中间件函数又返回了一个新的函数这里

这个next,就是接受到的参数,next必须是一个函数,因为需要嵌套的调用

如果把示例的代码平铺开来,应该是这样的

const composedMiddlewareA = action => {    console.log("before middlewareA");    console.log("middlewareA action =>", action);    // 这里的next,其实是一个函数参数,也就是middlewareB    // 因为调用了compose后的函数,所以这里的函数已经是返回的新函数了    // next(action)    (action => {        console.log("before middlewareB");        console.log("middlewareB action =>", action);        // 调用middlewareB的next        // middlewareB的参数,就是调用组合后函数传入的参数,在这里就是handle函数        // next(action)        (() => {            console.log("处理中");            console.log("处理完毕");        })();        console.log("after middlewareB");    })(action);    console.log("after middlewareA");};

这样就一目了然了,从A函数开始,A函数接收了action参数,在调用next参数的时候,将action传入,这个next就是A函数接收的参数,也就是B函数返回的新函数,在B函数里又调用了next函数,因为只有两个中间件,所以这里的next就是handle函数

中间件的思路理清楚后,再看redux的中间件就很清楚了,它的中间件又额外接收一个参数middlewareAPI返回了个新函数,让中间件函数拥有dispatchgetState的能力

当我明白了这个原理后,我再看见redux-thunk的源码只有十行的时候,已经在预期之中了

redux-thunk的简单实现

const thunk = ({ dispatch, getState }) => next => action =>  typeof action === "function" ? action(dispatch, getState) : next(action);

action是函数的时候,将dispatchgetState传入,原理就是这么简单

不过似乎中间件还有中间件,就像composeWithDevTools(applyMiddleware(thunk, logger))这样,用了浏览器的redux-dev-tool,又对applyMiddleware中间件再来了一次中间件

redux的源码,简直是闭包的典范,几乎全是闭包,我觉得写的是相当的牛皮了,中间件的实现简直是精妙绝伦

macbook居然自己烧焦了,迷醉,不得不承认,没有mac的日子很难受,已经有点不习惯windows了……