redux中间件实现

199 阅读10分钟

reduxmiddleware

现在我们希望每次状态更改时,打印出状态更改前后的变化

我们知道状态的更改是redux内部触发了diaptch才更改的

如果我们要完成上面的需求,书写逻辑就应该是

console.log('老状态', store.getState());
dispatch(action)
console.log('新状态', store.getState());

当然我们不可能为了这个需求就跑到redux内部源码去添加这两句

我们可以在创建store使用代理的思想来做个劫持

let store = createStore(reducers);

let dispatch = store.dispatch

store.dispatch = function (action) {
  console.log('老状态', store.getState());
  dispatch(action)
  console.log('新状态', store.getState());
}

image-20190905083556958

这样就可以完美的实现了我们的需求

compose

如果又有新的需求

let store = createStore(reducers);
/**
 * 中间件1
 */
let dispatch = store.dispatch

store.dispatch = function (action) {
	console.log('老状态', store.getState());
	dispatch(action)
	console.log('新状态', store.getState());
}
/**
 * 中间件2
 */
let dispatch_2 = store.dispatch

store.dispatch = function (action) {
	console.log('老状态__2', store.getState());
	dispatch_2(action)
	console.log('新状态__2', store.getState());
}

image-20190905134942192

这个打印顺序立马就让我们想到了洋葱模型

image-20190905135204944

image-20190905135601486

从洋葱图和上面代码的引导

我们知道所谓的中间件就是一个增强的dispatch

对于A来说B就是一个dispatch

这就像流水线上产品

image-20190905140742954

为了把上面的中间件代码优化

我们先来思考下怎么用函数的方式完成这种流水线加工的过程

function add1(dispatch) {
	return '|→' + dispatch + '←|'
}
function add2(dispatch) {
	return '||-→' + dispatch + '←-||'
}
function add3(dispatch) {
	return '|||--→' + dispatch + '←--|||'
}

let dispatch = add3(add2(add1('dispatch')))
console.log(dispatch);
|||--→||-→|→dispatch←|←-||←--|||

我们现在的需求是有一个compose方法

// let dispatch = add3(add2(add1('dispatch')))
// console.log(dispatch);
let add = compose(add3, add2, add1)
let dispatch = add('dispatch')

调用compose方法后他会把全部加工工序化为一个总的加工工序add

那么我们调用时只需要调用add()并把原材料传递进入

就能达到上面的效果


function compose(...fns) {
  // 返回全部加工工序的合计,即上面的 add
  return function (...args) { // 接受原材料 即 dispatch
    // 取出最后一个加工工序 即 add1
    let last = fns.pop()
    fns.reduceRight(() => {

    }, last(...args))
  }
}

这里使用了reduceRight

let arr = ['c', 'b', 'a']
let last = arr.pop()
console.log(last); // 'a'
console.log(arr); // [ 'c', 'b' ]
let ret = arr.reduceRight((prev, cur) => prev + cur, last)
console.log(ret); // abc

reduceRight表示从右往左执行

回调函数(prev, cur) => prev + cur里的参数prev是该函数上次执行的返回结果

如果reduceRight有第二个参数,如reduceRight((prev, cur) => prev + cur, last)这里的last

那么就会作为第一次执行回调函数的prev

所以执行第一次回调时

(prev, cur) => prev + cur

上面的prev就是last即,a

然后cur就是arr从右往左开始的数,即b

所以代码执行的就是'a' + 'b'然后返回a+b的结果,即'ab'

第二次执行回调时

(prev, cur) => prev + cur

此时的prev就是第一次回调的返回的值,即ab

cur就是c,所以最后的结果就是 'ab'+'c'abc

现在再回到代码里来

function compose(...fns) {
  console.log(fns); //[ [Function: add3], [Function: add2], [Function: add1] ]
  
  // 返回全部加工工序的合计,即上面的 add
  return function (...args) { // 接受原材料 即 dispatch
    console.log(args); //[ 'dispatch' ]
    // 取出最后一个加工工序 即 add1
    let last = fns.pop()
    console.log(last); // [Function: add1]
    
    // 加工工序的合计 即 add 最后把加工的物品返回
    return fns.reduceRight((val, fn) => fn(val), last(...args))
  }
}

上面的核心就只是fns.reduceRight((val, fn) => fn(val), last(...args))

  1. 第一次执行回调(val, fn) => fn(val)

    这里的val就是last(...args),即我们代码中的add1('dispatch')

    这里的fn就是fns从右往左的第一个

    fn(val)展开的代码就是add2(add1('dispatch'))

  2. 第二次执行回调(val, fn) => fn(val)

    此是的val就是上次回调的返回结果,即add2(add1('dispatch'))

    那么fn就是add3

    所以fn(val)就是add3(add2(add1('dispatch')))

此时fns.reduceRight就全部执行完毕,最后返回的就是add3(add2(add1('dispatch')))

所以上面的代码就是

function compose(...fns) {
	return function (...args) {
		return add3(add2(add1(...args)))
	}
}
let add = compose(add3, add2, add1)

此时的add就是

let add = function (...args) {
  return add3(add2(add1(...args)))
}
let dispatch = add('dispatch')

我们现在来回顾下我们做了什么

  1. 我们有一个dispatch

  2. 我们希望这个dispatch在流水线上加工

    let dispatch = add3(add2(add1('dispatch')))
    
  3. 我们希望把全部工序合并,调用合并后的接口即可

    let add = compose(add3, add2, add1)
    let dispatch = add('dispatch')
    
    1. 我们创建一个compose函数

      1. 接受所有加工工序

        compose(add3, add2, add1)
        
      2. 返回合并后的工序,调用合并后的工序即可返回最终结果

        let add = compose(add3, add2, add1)
        let dispatch = add('dispatch')
        

compose的核心就是使用reduceRight

  1. 接受加工工序的集合
  2. 返回一个总的加工工序
function compose(...fns){
  return function addAll(){
    
  }
}

那么这个addAll的参数就是原材料了

function addAll(...args){
	// ret1 = fn1(...args)
  // ret2 = fn2(ret1)
  // ret3 = fn3(ret2)
  // ...
  // ret = fnN(retN)
  return ret
}

因为每次fn调用的都是上一个函数执行后返回的结果

我们立马就会想到reduce

let arr = [1, 2, 3]
let ret = arr.reduce((prev, cur) => prev + cur) 
console.log(ret); // 6

最终我们就完成了

function compose(...fns){
  return function(...args){
    let last = fns.pop()
    return fns.reduceRight((val,fn) => fn(val),last(...args))
  }
}

好了 总结完了

但是这个compose写的还有点复杂

还有一个更优雅的写法

let add = compose(add3, add2, add1)
function compose(...fns) {
	return fns.reduce((a, b) => (...args) => a(b(...args)))
}

现在有了上面reduceRight的基础,我们再来看reduce就容易多了

reduce就是从左到右的,而且这里没有使用reduce的第二个参数

  1. 第一次执行回调

    因为这里没有第二个参数,所以a就是fns里的第一个元素,即add3

    那么b就是add2

    所以第一次回调返回的是

    function (...args){
      return add3(add2(...args))
    }
    
  2. 第二次执行回调

    此时的a就是上面的返回结果,即

    let a = function (...args){
      return add3(add2(...args))
    }
    

    b就是add1

    a(b(...args))
    

    这里就是最巧妙的地方了

    我们知道a现在是

    let a = function (...args){
      return add3(add2(...args))
    }
    

    我们现在把a的参数,即(...args)替换是b,即add1(...args)

    function (...args){
      return add3(add2(add1(..args)))
    }
    

    这样就达到上面的效果了

    这里巧妙的就是回调函数返回的是一个函数

    (...args) => prev(cur(...args))
    

    然后把cur作为该函数的参数就可以达到嵌套的效果即,add3(add2(add(...args)))

    当然这里有个前提是 fns的长度要等于2

    所以我们要添加一个句

    function compose(...fns) {
      if (fns.length === 1) return fns[0]
      return fns.reduce((a, b) => (...args) => a(b(...args)))
    }
    

中间件写法

我们说了这么久的compose差点快忘了我们的初衷

let store = createStore(reducers);
/**
 * 中间件1
 */
let dispatch = store.dispatch

store.dispatch = function (action) {
  console.log('老状态', store.getState());
  dispatch(action)
  console.log('新状态', store.getState());
}
/**
 * 中间件2
 */
let dispatch_2 = store.dispatch

store.dispatch = function (action) {
  console.log('老状态__2', store.getState());
  dispatch_2(action)
  console.log('新状态__2', store.getState());
}

因为我们知道其实所谓的中间件就是对dispatch进行加工

image-20190905135204944

image-20190905140742954

我们现在希望的是

let dispatch = store.dispatch 

let middleware1 = {}
let middleware2 = {}
let middleware3 = {}

let middleware = compose(middleware1, middleware2, middleware)

store.dispatch = middleware

这个合并的目标我们已经达成了

现在我们的问题来了

我们应该怎么把

/**
 * 中间件1
 */
let dispatch = store.dispatch

store.dispatch = function (action) {
  console.log('老状态', store.getState());
  dispatch(action)
  console.log('新状态', store.getState());
}

转换成一个函数形式的

let middleware1 = {}

首先中间件是一个dispatch,那么框架就是

let middleware = function(action){
  
  dispatch(action)
  
}

根据我们加工厂的例子

function add1(dispatch) {
  return '|→' + dispatch + '←|'
}
function add2(dispatch) {
  return '||-→' + dispatch + '←-||'
}
function add3(dispatch) {
  return '|||--→' + dispatch + '←--|||'
}

let dispatch = add3(add2(add1('dispatch')))
console.log(dispatch);

我们的中间件应该要接受一个dispatch以确定获得是加工过的dispatch

那我们就把代码改成

let middleware = function(dispatch){
  return  function(action){
    // 逻辑代码
    dispatch(action)
    // 逻辑代码
  }
}

那到时我们就把middleware()执行再返回,就达到了以下两个需求

  1. middleware返回一个加工后的 dispatch需求
  2. middleware对流水线上的dispatch进行加工

现在一切都很完美

但是我们突然想到我们的逻辑代码

/**
 * 中间件1
 */
let dispatch = store.dispatch

store.dispatch = function (action) {
  console.log('老状态', store.getState());
  dispatch(action)
  console.log('新状态', store.getState());
}

会使用getState

这就尴尬了,我们这个getState没有办法获取

于是我们只能使用老套路,用函数套函数的方式

let middleware = function(store){
  return function(dispatch){
    return function(action){
      // 逻辑代码
      // store.getState()
      dispatch(action)
      // 逻辑代码
    }
  }
}

那就有人好奇为什么不直接

let middleware = function(store){
  return  function(action){
    // 逻辑代码
    store.dispatch(action)
    // 逻辑代码
  }
}

因为这样我们的层次就没有了,参数的位置也需要规定

而且使用函数套函数的写法能使得该函数具有更高的可复用性,即解耦合

因为我们有时候的需求可能只是

fn()返回的结果

有时候的需求又可能是fn()()返回后的结果

let store = createStore(reducers);
let dispatch = store.dispatch

let middleware1 = {}
let middleware2 = {}
let middleware3 = {}

let middlewares = [middleware1, middleware2, middleware3]
// 获取到了 store 这样就可以使用 store.getState
middlewares = middlewares.map(middleware => middleware(store))
// 流水线合并 获取总的加工工序集合
add = compose(...middlewares)
// 往加工工序里添加原材料 即 最原始的 store.dispatch
store.dispatch = add(dispatch)

那我们就知道中间件的最终写法就是

let middleware = function ({ getState }) {
  return function (dispatch) {
    return function (action) {
      // 逻辑代码 getState
      dispatch()
      // 逻辑代码 getState
    }
  }
}

applyMiddleware

我们通过使用compose和确定middleware的方法使得中间件可以复用

但是代码还是特别的冗长

let store = createStore(reducers);
let dispatch = store.dispatch

let middleware1 = {}
let middleware2 = {}
let middleware3 = {}

let middlewares = [middleware1, middleware2, middleware3]
middlewares = middlewares.map(middleware => middleware(store))
add = compose(...middlewares)
store.dispatch = add(dispatch)

我们希望把以下三个过程合并成一个函数

// 创建store
let store = createStore(reducers);
// 截取 store.dipatch
let dispatch = store.dispatch

// 给store.dispatch 绑定成我们加工后的 dispatch
let middlewares = [middleware1, middleware2, middleware3]
middlewares = middlewares.map(middleware => middleware(store))
let add = compose(...middlewares)
store.dispatch = add(dispatch)

我们最终期望的是执行一个函数就返回好处理过dispatchstore

const store = applyMiddleware(...)

那我们就知道

let applyMiddleware = function () {
  let store = createStore(reducer)
  let dispatch = store.dispatch
  let middlewares = [middleware1, middleware2, middleware3]
  middlewares = middlewares.map(middleware => middleware(store))
  store.dispatch = compose(...middlewares)(dispatch)
  return store
}

这样写确实是使用函数完成了,但是我们思考一下我们应该怎么写参数呢

applyMiddleware就一定会接受三个参数

  1. middlewares
  2. reducer
  3. createStore

这个时候我们就会又想到中间件的函数套函数的写法

来确定参数的层级关系和解耦合

那我们就把上面三个参数分有三层来处理

如果是你你会思考着按什么顺序调用呢?

既然叫applyMiddleware那我们当然希望最贴近的参数是Middleware

const store = applyMiddleware(middleware1,middleware2,middleware3)

那我们还要传递createStorereducer

既然平时使用这两个参数是使用

let store = createStore(reducer)

我们就按这种顺序传递

const store = applyMiddleware(middleware1,middleware2,middleware3)(createStore)(reducer)

applyMiddleware的写法应该就是

let applyMiddleware = function (...middlewares) {
	return function (createStore) {
		return function (reducer) {
			let store = createStore(reducer)
			let dispatch = store.dispatch
			middlewares = middlewares.map(middleware => middleware(store))
			store.dispatch = compose(...middlewares)(dispatch)
			return store
		}
	}
}

代码已经可以跑通了

我们思考下middleware(store)

我们突然觉得不应该直接把整个store传递给他

因为store上有很多方法的

如果我们在写中间件哪里不小心调用了

这个错误就很难找到了

let middlewareAPI = {
  getState: store.getState,
}
middlewares = middlewares.map(middleware => middleware(middlewareAPI))

我们还发现

let dispatch = store.dispatch
store.dispatch = compose(...middlewares)(dispatch)

这里写的很累赘,合并起来就可以了

let applyMiddleware = function (...middlewares) {
  return function (createStore) {
    return function (reducer) {
      let store = createStore(reducer)
      let middlewareAPI = {
        getState: store.getState,
      }
      middlewares = middlewares.map(middleware => middleware(middlewareAPI))
      store.dispatch = compose(...middlewares)(store.dispatch)
      return store
    }
  }
}

createStore

其实,说实在的

const store = applyMiddleware(logger, logger1)(createStore)(reducer)

这种写法真的挺丑的

我们看官方文档提供了一种比较好的写法

const store = createStore(reducer, ['Use Redux'], applyMiddleware(logger, logger1))

这就使用到了createStore的第二个和第三个参数

export default function createStore(reducer, preloadedState, enhancer) {
  if (enhancer) {
    return enhancer(createStore)(reducer, preloadedState);
  }
  //...
}

仔细一看就知道只是把后面的执行部分放在了源码里面实现

从这里我们就更可以明白了为什么使用函数套函数的写法了

因为这里我们只是需要applyMiddleware()的结果作为值返回

如果我们使用了applyMiddleware(middlewares,createStore,reducer)的写法

那``applyMiddleware`就写死了