深入理解Redux中间件

2019-06-27  本文已影响0人  7revor

思考

我们在 Redux 异步 Action 中经常使用这种写法:

export function getTodos(){
  return (dispatch,getStore) => {
    TodoApi.getTodos().then(result=>{
      dispatch({
      type:TODOLIST,
      data:result.data
      })
    })
  }
}

而正常的 Action 就看起来好像应该是这样:

export function getTodos(){
  return {
    type:TODOLIST,
    data:result.data
  }
}

为什么我们上一种写法能正常工作呢?Action不应该返回一个对象吗怎么返回了一个函数?

Action 之所以支持第一种写法,是因为我们引入了 redux-thunk 中间件。那么 redux-thunk 是怎么工作的?

让我们在本文中一步步剖析 middleware 中间件的实现原理。

什么是 middleware

正因为 middleware 可以完成包括异步 API 调用在内的各种事情,了解它的演化过程是一件相当重要的事。

我们将以记录日志和创建崩溃报告为例,体会从分析问题到通过构建 middleware 解决问题的思维过程。

问题:记录日志

现在设想这么一个问题:我们需要在应用中每一个 Action 被发起以及新的 state 被计算完成时都将他们记录下来。当程序出现问题时,我们可以通过查阅日志找出是哪个 action 导致了 state 不正确。

尝试 1: 手动记录

假设,我们在获取Todo列表时这么调用

export function getTodos(){
  return {
    type:TODOLIST,
    data:result.data
  }
}
store.dispatch(getTodos())

为了记录这个action以及新的state,我们可以通过这种方式记录:

let action = getTodos();
console.log('dispatching', action)
store.dispatch(action)

虽然这么做可以达到想要的效果,可是我们并不想每次都这么多。

尝试 2: 封装 Dispatch

我们可以将上面的操作封装成一个函数

function dispatchWithLog(store,action){
    console.log('dispatching', action)
    store.dispatch(action)
}

然后我们这么调用它:

dispatchWithLog(store, getTodos())

我们已经接近了 middleware 的思想,但每次都要导入一个外部方法总归不大方便。

尝试 3: 替换 Dispatch

如果我们直接替换 store 实例中的 dispatch 函数会怎么样呢?

Redux store 只是一个包含一些方法的普通对象,同时我们使用的是 JavaScript,因此我们可以这样来包装 dispatch:

let next = store.dispatch
store.dispatch = function dispatchWithLog(action) {
  console.log('dispatching', action)
  let result = next(action)
  console.log('next state', store.getState())
  return result
}

这样就完美地改写了dispatch,保留了原始功能,还添加了自定义的方法。离我们想要的已经非常接近了!

但直接替换 dispatch 令人感觉还是不太舒服,不过利用它我们做到了我们想要的。

问题: 捕获异常

如果我们想对 dispatch 附加超过一个的特殊处理,又会怎么样呢?

脑海中出现的另一个常用的特殊处理就是在生产过程中报告 JavaScript 的错误。

但是全局的 window.onerror 并不可靠,因为它在一些旧的浏览器中无法提供错误堆栈,而这是排查错误所需的至关重要信息。

试想当发起一个 action 的结果是一个异常时,我们将包含调用堆栈,引起错误的 action 以及当前的 state 等错误信息通通发到报告服务中,不是很好吗?这样我们可以更容易地在开发环境中重现这个错误。

然而,将日志记录崩溃报告 分离是很重要的。理想情况下,我们希望他们是两个不同的模块,也可能在不同的包中。否则我们无法构建一个由这些工具组成的生态系统。

按照我们的想法,日志记录和崩溃报告属于不同的模块,他们看起来应该像这样:

function patchStoreWithLog(store) {
  let next = store.dispatch
  store.dispatch = function dispatchWithLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function patchStoreWithReport(store) {
  let next = store.dispatch
  store.dispatch = function dispatchWithReportErrors(action) {
    try {
      return next(action)
    } catch (error) {
      console.error('捕获一个异常!', error)
      report({
          error, 
          action,
          state: store.getState()
      })
      throw error
    }
  }
}

如果这些功能以不同的模块发布,我们可以在 store 中像这样使用它们:

patchStoreWithLog(store)
patchStoreWithReport(store)

这样我们就实现了对dispatch的多重处理。

尽管如此,这种方式看起来还是有一些啰嗦。

尝试 4: 隐藏 hack

我们之前的操作本质上是一种hack。

我们用自己的函数替换掉了 store.dispatch。如果我们不这样做,而是在函数中返回新的 dispatch 呢?

function logger(store) {
  let next = store.dispatch
  // 我们之前的做法:
  // store.dispatch = function dispatchAndLog(action) {
  return function dispatchWithLog(action) {
    console.log('dispatching', action)
    let result = next(action)
    console.log('next state', store.getState())
    return result
  }
}

function report(store){
  let next = store.dispatch
    // 我们之前的做法:
    // store.dispatch = function dispatchAndLog(action) {
    return function dispatchWithReport(action) {
    try{
      let result = next(action)
      return result
      }catch (error) {
          console.error('捕获一个异常!', error)
            report({
               error, 
               action,
               state: store.getState()
            })
          throw error
      }
    }
}

我们通过闭包存储了store,以便在action真正调用的时候供其访问。

我们可以在 Redux 内部提供一个可以将实际的 hack 应用到 store.dispatch 中的辅助方法:

function applyMiddleware(store, middlewares) {
  // 在每一个 middleware 中变换 dispatch 方法。
  middlewares.forEach(middleware =>
    store.dispatch = middleware(store)
  )
}

然后像这样应用多个 middleware:

applyMiddleware(store, [ logger,report ])

上面的代码可能看起来一时难以理解,这也是中间件的精华所在。我们来具体分析一下:

middleware

每一个middleware都是高阶函数,它使用了函数柯里化的思想,分两步执行:

applyMiddleware

接收原始的store对象和一系列的middlewares

它遍历 middlewares 列表,进行以下操作:

这样一来,每一个 middleware 所存储的 store ,都是被上一个 middleware hack后的。也就是说,
我们在 report 中闭包存储的store对象,它的 dispatch 方法实际上是 logger 的dispatchWithLog

总体流程

尝试 #5: 移除 hack

为什么我们要替换原来的 dispatch 呢 ?
就是每一个 middleware 都可以操作前一个 middleware 包装过的 store.dispatch。

如果没有在第一个 middleware 执行时立即替换掉 store.dispatch,
那么 store.dispatch 将会一直指向原始的 dispatch 方法。也就是说,第二个 middleware 依旧会作用在原始的 dispatch 方法。

还有另一种方式来实现这种链式调用的效果。就是将middleware柯里化为三步,
让 middleware 以方法参数的形式接收一个 next() 方法,而不是通过 store 的实例去获取,这样我们就可以避免对store.dispatch的hack

function logger(store) {
  return function wrapDispatch(next) {
    return function dispatchWithLog(action) {
      console.log('dispatching', action)
      let result = next(action)
      console.log('next state', store.getState())
      return result
    }
  }
}

这些串联函数很吓人。ES6 的箭头函数可以使 柯里化 看起来更舒服一些:

const logger = store => next => action => {
     console.log('dispatching', action)
     let result = next(action)
     console.log('next state', store.getState())
     return result
}
const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    console.error('Caught an exception!', err)
    Raven.captureException(err, {
      extra: {
        action,
        state: store.getState()
      }
    })
    throw err
  }
}

这正是 Redux middleware 的样子。

如果要自己实现一个 middleware 应用到 redux 中,完全可以按照这种形式去写。

源码分析

我们的applyMiddleware 和 Redux 中 applyMiddleware() 的实现已经很接近了。有了上面的铺垫,让我们来分析一下真正的源码。

代码虽然只有不到20行,但看懂确实是不容易。

export default function applyMiddleware(...middlewares) { 
  return createStore => (...args) => { 
    const store = createStore(...args)  
    let dispatch = () => { 
      throw new Error(
      )
    }
    const middlewareAPI = {  // 定义API
      getState: store.getState, //注入 getStore方法
      dispatch: (...args) => dispatch(...args) //初始化dispatch
    }
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

middlewares

export default function applyMiddleware(...middlewares)

applyMiddleware接收第一个参数,他正是所有的middleware列表。

createStore 以及 reducers

return createStore => (...args) => {
  ... ...
}

applyMiddleware接收第二和第三个参数,他们分别是createStore reducers

为了保证只能应用 middleware 一次,它作用在 createStore() 上而不是 store 本身。

创建store

 const store = createStore(...args)  

利用传入的createStore和reducer和创建一个store

定义API

let dispatch = () => { 
      throw new Error(
      )
    }
    const middlewareAPI = {  // 定义API
      getState: store.getState, //注入 getStore方法
      dispatch: (...args) => dispatch(...args) //初始化dispatch
    }

这里有一个地方需要注意:

初始化

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

让每个 middleware 带着 middlewareAPI 这个参数分别执行一遍,进行初始化。

得到的函数链为每个中间件的第一个返回函数,该函数可接收一个dispatch动作,再返回一个可以接收action的函数。

函数链中的每一个函数看起来像是这样:

middlewareAPI = {  
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
} 
// 这里 middlewareAPI 作为闭包存在于匿名函数Anonymous的作用域链
function Anonymous (next) {
  return function(action){
    ... ...
    return next(action);
  }
}

它的柯里化后两步操作分别为:

compose

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

这句是最精妙也是最有难度的地方。

个人认为这个compose函数是整个redux中非常亮眼的部分,
短短几行代码,就完成了一个核心功能的扩展,是责任链设计模式的经典体现。
我们来看一下compose的源码:

function compose() {
  for (var _len = arguments.length, funcs = Array(_len), _key = 0; _key < _len; _key++) {
    funcs[_key] = arguments[_key];
  }

  if (funcs.length === 0) {
    return function (arg) {
      return arg;
    };
  }

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

  return funcs.reduce(function (a, b) {
    return function () {
      return a(b.apply(undefined, arguments));
    };
  });
}

compose在这里将所有的 中间件的第一个返回函数 聚合。
也就像我们刚才分析的,将store.dispatch传给第一个中间件,第一个中间件对其进行封装后传给第二个中间件,
以此类推... ...。

最底层的dispatchstore.dispatch,一层一层的封装,最终得到一个层层封装后的“dispatch”

或许已经不能称之为dispatch,他是原生的dispatch,以及一系列增强函数的集合

最后

return {
  ...store,
  dispatch
}

store中的所有可枚举属性复制进去(浅复制),并用层层封装好的“dispatch”覆盖store中的dispatch属性。

redux-thunk

我们回到最开始的问题

export function getTodos(){
  return (dispatch,getStore) => {
    TodoApi.getTodos().then(result=>{
      dispatch({
      type:TODOLIST,
      data:result.data
      })
    })
  }
}

上面这种写法是怎么工作的呢?

让我们结合redux-thunk源码来分析一下便一目了然。

function createThunkMiddleware(extraArgument) {
  return function (_ref) {
    var dispatch = _ref.dispatch,
      getState = _ref.getState;
    return function (next) {
      return function (action) {
        if (typeof action === 'function') {
          return action(dispatch, getState, extraArgument);
        }
        return next(action);
      };
    };
  };
}

代码同样精炼,改造后的dispatch接收到action后有两种情况:

{
  type:GETTODOS,
  data:[]
}

那么,redux-thunk将不予处理,继续将 action 向下传递。项目中如果没有其他中间件,这里会直接调用原生的dispatch,交由 Reducer 处理。

  return (dispatch,getStore) => {
    TodoApi.getTodos().then(result=>{
      dispatch({
      type:TODOLIST,
      data:result.data
      })
    })
  }

为了方便理解,我们可以稍作变换:

const func = (dispatch, getStore) => {
  TodoApi.getTodos().then(result => {
    dispatch({
      type: TODOLIST,
      data: result.data
    })
  })
}
return func;

这里的返回值无疑是function。现在就轮到redux-thunk上场了:

if (typeof action === 'function') {
    return action(dispatch, getState, extraArgument);
}

这里的action就是我们的func,func在此处被执行,就相当于:

  func(dispatch, getState, extraArgument);

这里的dispatchgetState都是在闭包中存储的变量。

看到这里,应该不难明白这是如何工作的了吧:

export function getTodos(){
  return (dispatch,getStore) => {
    TodoApi.getTodos().then(result=>{
      dispatch({
      type:TODOLIST,
      data:result.data
      })
    })
  }
}
上一篇 下一篇

猜你喜欢

热点阅读