redux 源码研究:中间件

设计模式与 redux 中间件

中间件是代理/装饰模式的一种的实践方式,通过改造 store.dispatch 方法,可以拦截 action(代理)或添加额外功能(装饰)。

突然发现 Javascript 里的代理/装饰模式的写法蛮通用的....

对于创建的 store 对象,如果希望代理/装饰 dispatch 函数,基本的格式如下:

  1. 新建一个变量指向 store.dispatch。
  2. 新建同名函数 dispach,接收参数为 action。
  3. 编写自己的额外逻辑。
  4. 在 dispach 内部执行 oldDispatch,并返回( store.dispatch 是有返回值的)。
  5. 令 store.dispatch 指向新的 dispatch ,返回新的 store。

简单的说:函数地址交换,在新函数中执行老函数。

const applyMyMiddlware = (store) => {
  // 1. 新建一个变量指向 store.dispatch
  const oldDispatch = store.dispatch;

  // 2. 新建 dispach,接收参数为 action
  const dispatch = (action) => {

    // 3. 编写额外逻辑
    /* 
      ........
    */

    // 3.1 所谓的代理就是拦截参数 action,根据 action 来进行自己的操作
    // 3.2 所谓的装饰就是不拦截 action,但是在这之前进行自己的逻辑处理
    // 3.3 注意对象中 this(如果有) 的指向问题

    // 4. 在 dispach 内部执行 oldDispatch,并返回。
    return oldDispatch(action);
    // 4.1 store.dispatch 是有返回值的,返回值类型是 action
  };
  //5 令 store.dispatch 指向新的 dispatch ,返回新的 store
  store.dispatch = dispatch;

  return store
  // 或者也可以这样写
  //  returen {
  //    ...store,
  //    dispatch
  //  } 
}复制代码

执行 store = applyMyMiddlware(store) 后, 调用 store.dispatch(action) 的结果便为代理/装饰后的结果。

applyMiddleware 源码研究

redux 提供了官方加载中间件的函数 applyMiddleware,同时规定了中间件的写法必须是:

({dispatch, getState}) => next => action => {
  // .... 中间件自己的逻辑

  return next(action);
}复制代码

直到看源码之前,我只是单纯的记住了这么一长串的多重调用,并不理解为什么。

而这种多重返回的原因,就在 applyMiddleware 的源码里

import compose from './compose'

export default function applyMiddleware(...middlewares) {
  return (createStore) => (...args) => {
    const store = createStore(...args)
    let dispatch = () => {
      throw new Error(
        `Dispatching while constructing your middleware is not allowed. ` +
        `Other middleware would not be applied to this dispatch.`
      )
    }
    let chain = []

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }
    chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch
    }
  }
}复制代码

1. ({dispatch, getState})

分析一下 applyMiddleware 源码很容易找到 {dispatch, getState} 的来源:

  • 首先用传入的 createStore 方法创建了 store 对象。此时 store 中有 store.dispatch 以及 store.getState 方法(subscribe 暂时不考虑)。

  • 初始化了一个 dispatch,但是中间塞了一个断言。如果直接调用,就会报错。

  • 定义了一个 chain 数组和

  const middlewareAPI = {
    getState: store.getState,
    dispatch: (...args) => dispatch(...args)
  }复制代码

getState 可以获得当前 state 状态,dispatch 则是经过处理添加了新功能

  • 通过 map 函数,把每个中间件执行了一遍,传入的参数就是 middlewareAPI。
 chain = middlewares.map(middleware => middleware(middlewareAPI))复制代码

因此,对于中间件:

const middleware = ({dispatch, getState}) => next => action => {
  // .... 中间件自己的逻辑

  return next(action);
}复制代码

第一个参数 {dispatch, getState} 显然是 (middlewareAPI),返回值为

 next => action => {
  // .... 中间件自己的逻辑

  return next(action);
}复制代码

这么调用的好处是,在返回值(也是个函数)内部依然可以调用到 store.getState 方法闭包

2. next 是什么(上):丧心病狂的 compose

经过 map 遍历,chain 数组此刻的值为:

[
  next => action => {
    // .... 中间件自己的逻辑

    return next(action);
  },
  next => action => {
    // .... 中间件自己的逻辑

    return next(action);
  } 
  // ...其他中间件
]复制代码

这么一种形式。

dispatch = compose(...chain)(store.dispatch),是整段代码中最不()好()理()解()的部分。

贴一下 compose 函数源码:

export default function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}复制代码

根据其源码,compose 应对了 3 种数组的情况。

对于 dispatch = compose(...chain)(store.dispatch)

  • 如果 chian 的长度是 0(也就是未传入中间件),等价于 dispatch = (args=> args)(store.dispatch) ,即 dispatch = store.dispatch。
  • 如果 chian 的长度为 1,也就是说为
    [
      next => action => {
        console.log('0号中间件')
        return next(action)
      }
    ];复制代码
    如果用一个变量 temp0 指向上述函数的地址,compose(...chain)(store.dispatch) 便等同于 temp0(next = store.disptach)此时 next 为 store.dispatch,next(action) 便等同于 store.dispatch(action)。

因此 temp0(next = store.disptach) 的返回值应为:

  dispatch = temp0(next = store.disptach) = action => {
    console.log('0号中间件');
    return store.dispatch(action);
  }复制代码

结论:

无中间件的情况下, dispatch 为 store.dispatch在只有一个中间件的情况下,next 的值是 store.dispatch。把 console.log('0号中间件'); 换成其他的逻辑,中间件就可以在保证原本 store.dispatch 功能的情况下,实现自己的额外功能。

3. next 是什么(下):庶民推理

接下来的推理相当绕,我也是捋了半天才捋明白。

懒人请直接看结论。

结论1:当多于 2 个的元素的时候,传入的 action 按照初始化时中间件数组的顺序依次经过每个中间件,最后依靠执行 store 原本的 dispatch (当然前提是此 action 没有被中途拦截)离开,完成整个流程。

注:单向数据流

结论2:对于每一个中间件({dispatch, getState}) => next => action => {..... return next(action)} ,如果不做拦截 action 最早传入的 action;如果进行拦截,后面的 action 为拦截后生成的 action。最后一位中间件的 next 为 store.dispatch,其余中间件的 next 都是下一位中间件 action => {..... return next(action)} 的部分。

以下是无聊的推理过程:

  • 假设 chain 的长度 为 2 。

即:

  // 先假设只有 2 个元素
  chain = [
      next => action => {
          console.log('0 号中间件');
          return next(action);
      }, 
      next => action => {
          console.log('1 号中间件')
          return next(action);
    }]复制代码

脑补 compose 的执行过程。

第 1 步. return funcs.reduce((a, b) => (...args) => a(b(...args)))

因为没有初始值,所以 a b 为最开始的两个元素。即

return (...args) => a(b(...args)));

compose(...chian) = (...args) => a(b(...args)));

数组的 reduce 方法很有意思,接收一个回调函数(和一个初始值)做参数。该回调函数的第一个参数便是该回调函数上一次执行的结果。如果有初始值用初始值,如果没有初始值则直接从第二个元素开始循环,初始值为第一个元素。常用于解决递归的逻辑,和 map 相比最大的好处是不用引入外界变量。

第 2 步. 根据 JavaScript 的语法,先执行 b(...args)

b 为

next=> action => {
    console.log('1 号中间件')
    return next(action);
  }复制代码

所以 b(...args) 的执行结果为

action => {
    console.log('1 号中间件')
    return (...args)(action);
  }复制代码

第 3 步. 执行 a(b(...args))

a 为:

next => action => {
    console.log('0 号中间件');
    return next(action);
  }复制代码

a(b(...args)) 就等同于

  action => {
      console.log('0 号中间件');
      return (
        // 用 b(...args) 的返回值代替 next
        action => {
          console.log('1 号中间件')
          return (...args)(action);
        }
      )(action)
}复制代码

compose(...chian)

  (...args) => action => {
      console.log('0 号中间件');
      return (
        // 用 b(...args) 的值代替 next
        action => {
          console.log('1 号中间件')
          return (...args)(action);
        }
      )(action)复制代码

第 4 步:dispatch

dispatch 等价于 compose(...chian)(store.dispatch) 等价于

  // 因为 compose(...chian)(store.dispatch) 的参数 ...args 等于 store.dispatch
  // 去掉 (...args)=> 并应用 store.dispatch 替换 (...args)(action) 为 (store.dispatch)(action)
  dispatch = action => {
      console.log('0 号中间件');
      return (action => {
        console.log('1 号中间件')
          // 使用 store.dispatch 代替 ...args
        return (store.dispatch)(action);
      })(action);复制代码

换个写法:

dispatch = action => {
  console.log('0 号中间件');

  const next = action => {
    console.log('1 号中间件');
    return store.dispatch(action);
  }

  reutrn next(action);
}复制代码

3.1. 递归与中间件调用

现在考虑 chain 的数组多于 2 个元素的情况,例如 chain = [a, b, c]

由 3 得知 , b c 的执行结果是

dispatchBC = action => {
  console.log('b');

  const next = action => {
    console.log('c');
    return store.dispatch(action);
  }

  reutrn next(action);
}复制代码

因此 compose(...[a, b, c]) 的执行结果等同与

dispatch = action => {
  console.log(a);

  const next = dispatchBC;

  return next(action);

}复制代码

赞美递归!

后记:为什么要采用这种复杂的调用?

没看懂 applyMiddleware 源码之前总是觉得作者故意找茬,为什么不能用订阅/发布的模式去写?

比如像这个样子:

const applyObserver = (...middlewares) {
    // .... 创建 store 的逻辑
    const oldDispatch = store.dispatch;
    const dispatch = (action) => {
        for(middleware in middlewares) {
            middleware.call(null, {dispatch: store.dispatch, getState: store.getState})
        }
        return oldDispatch(action)
    }
    return {
        ...store,
        dispatch
    }
}复制代码

看懂了以后发自内心的赞叹:卧槽太牛逼了。

使用 applyObserver 只能实现装饰模式,无法实现对 action 的拦截与转换。如果每一个中间件都能消费或者产生新的 action,那么一个 action 传入后会产生多个 action,而这与 redux 单项数据流 的理念相悖。applyMiddleware 的写法最大程度的保证了 action 的流向,每一步的数据变化都是可以追踪的。

这边是 redux 中间件使用多重返回函数的真正原因。

这也是 compose 为什么这么牛逼的原因。

后记2:函数与 JavaScript

我刚开始用 js 的时候有位高人对我说:JavaScript 其实并不是正统的 OOP 函数。

确实,直到 ES6 里面才有了 extends 关键字进行继承,ES6 之前只有 proTotype。而所谓的 class 也不过是转成函数,进行调用。虽然 JavaScript 经过 es6 的革新和 es7 的强化后写法不再那么反人类,但是离纯 OOP 的语言比如 Java 还有不小的差距。

研究过 dva 和 redux 的部分源码之后,我发现 JavaScript 框架的作者在解决通用性问题的方式,都是通过提供了组合的函数而不是一个组合过的类( dva 处理异步调用的时候是返回了一个 takeEvery 的函数)。

没有什么不是一个函数可以解决的问题,如果有就再来一个

这个就和目前的 OOP 思想差别相当大了,瞄准的是功能而不是对象。

对于 Java,虽然可以使用反射实现动态调用,但是类必须真实存在的;
对于 JavaScript,有没有类无所谓,没有就自己造一个。只要产生的对象能嘎嘎叫并像鸭子一样走路,那就是鸭子(著名的鸭式辨型)。
现在前端推广 stateless 组件和高阶组件,写来写去也是函数。

OOP 用多了,有的时候是有思维盲区存在的;换个角度从函数出发,说不定真的有惊喜。

不过首要解决的还是如何习惯阅读返回值是函数的函数,坦白的说在这一点上我经常被绕晕,尤其是函数套函数的情况,超过 2 层必蒙圈(捂脸)。所以多重函数一直是我尽力避免或者绕开的问题,但是现在看来这才是 JavaScript 的精髓。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值