Redux源码解析(二)

147 阅读10分钟

中间件与异步操作

由于国庆前项目要上线,加上随之而来的国庆假期,redux系列的学习总结就被耽误了 ~~~今天终于可以抽出时间来整理总结了。本篇文章先从中间件说起,什么是中间件,为什么需要中间件、中间件解决了哪些问题,进而进入redux中的异步操作。

Redux中的中间件

在搞明白什么是中间件之前先看一个小需求: 想做一个类似日志功能,记录每次用户操作的actiondispatch之后的state值。在上一篇文章中我们知道用户dispatch一个action后,就会立即被reducer函数处理并返回一个新的state,接着调用subscribe中的监听函数执行页面的更新操作。

源码中的dispatch方法内部并没有给我们提供类似可扩展的功能,所以我们只能在用户调用dispatch前后,手动添加日志

console.log("prev state", store.getState())
const action = store.dispatch(action)
console.log("action", action)
console.log("next state", store.getState())

然后对功能进行封装

function enhancerDispatch(store, action) {
  console.log("prev state", store.getState())
  console.log("action", action)
  store.dispatch(action)
  console.log("next state", store.getState())
}

虽然功能我们已经封装好了,但这样的做法还是需要在每次使用时导入enhancerDispatch函数才行,有点麻烦呀,如果我们直接修改原store.dispatch的方法呢?是不是就只用在引入store时修改一次就可以了呢

const next = store.dispatch; //将store.dispatch原先的引用赋值给一个新的变量
store.dispatch = function (store, action) { //将一个我们自己定义的函数赋值给store.dispatch
  console.log("prev state", store.getState())
  console.log("action", action)
  next(action) //执行原dispatch方法
  console.log("next state", store.getState())
}

我们用自己的函数替换了store.dispatch,确实以后我们不管在任何地方使用store.dispatch都会带有日志的功能了;继续 我们再实现一个异常捕获的功能,显然这些功能最好都是彼此独立的,既可以同时使用,又能单独调用。这样的话那就不能将异常捕获的功能也参杂到日志记录的功能中了,最好以模块的形式将彼此分开单独写。

//日志
function loggerDispatch(store) {
  const next = store.dispatch; //将store.dispatch原先的引用赋值给一个新的变量
  store.dispatch = function (action) { //将一个函数赋值给store.dispatch
    console.log("prev state", store.getState())
    console.log("action", action)
    const result = next(action) //执行原dispatch方法, 默认返回action
    console.log("next state", store.getState())
    return result
  }
}

//异常
function errorDispatch(store) {
  const next = store.dispatch; //将store.dispatch原先的引用赋值给一个新的变量
  store.dispatch = function (action) { //将一个新函数赋值给store.dispatch
    console.log("error", action)
    try {
      return next(action) //执行原dispatch方法
    } catch (err) {
      console.log("error", err)
    }
  }
}

//让日志功能和异常报告功能同时生效
loggerDispatch(store)
errorDispatch(store)

开始的时候在这里我有一个困惑的地方,store.dispatch的值是一个引用数据类型,那么在执行第一个函数loggerDispatch(store)之后,我们就用自己的函数替换了原store.dispatch所指向的函数, 在执行完第二个函数errorDispatch(store)之后,最终store.dispatch就指向了上面我们写的异常捕获函数了,那为啥当我在组件中调用store.dispatch时,上面两个功能会同时生效呢?按理说store.dispatch指向了异常捕获函数,那不是应该只有异常捕获功能有效吗???

后来发现是有一行关键的代码自己没有理解清楚忽略了,关键之处就在于next(action),下面就来看看他的关键之处:

//函数一
function loggerDispatch(store) {
  const next = store.dispatch; //将原store.dispatch函数用一个变量保存
  store.dispatch = function (action) { //将一个新函数赋值给store.dispatch
    // ...
    const result = next(action) //这里执行的就是上面保存的那个函数
  }
}

//函数二
function errorDispatch(store) {
  const next = store.dispatch; //注意这里next变量保存的就是函数一第4行赋值的新函数
  store.dispatch = function (action) { //再一次将一个新函数赋值给store.dispatch
   // ...
     return next(action) //注意这里执行的就是函数一第4行赋值的新函数
  }
}
//按顺序调用函数一、函数二
loggerDispatch(store)
errorDispatch(store)

所以当在组件中执行store.dispatch时,此时的store.dispatch函数已经被替换成函数二第13行赋值的新函数了,当函数执行到next(action)时,这里实际上调用的是函数一中给store.dispatch新赋值的函数,当在这个新函数中执行到next(action)时,最后这里实际调用的才是原Redux库为我们提供的dispatch函数。我们也可以发现在这些功能函数被调用的时候,是由外往内在next(action)前后添加功能, 但是最终在组件中调用dispatch时,这些功能是由内往外体现,所以在console控制台中应该是先打印error,后打印logger

如果后面再需要添加别的功能,依然是像上面这样,用新函数替换掉Redux库为我们提供的store.dispatch。但是总感觉这种直接替换别人源码的方式怪怪的。我们上面之所以要用新函数替换掉原stroe.dispatch,就是为了在后面可以操作或调用上一层的dispatch方法 ,使所有扩展功能都生效。如果我们不去替换原来的dispatch,而是在函数中返回一个新的dispatch,然后类似像链式调用一样,在新函数中接受上一层返回的dispatch作为参数,这样新函数内部不也可以获取上一层的dispatch方法了吗。这里有没有一点 函数按照一定顺序并层层嵌套执行的感觉??想到了啥? 对!复合函数compose(将一个函数的返回值作为下一个函数的参数,并按照一定顺序执行)。

function logger(store) {
  //之前的做法是用一变量保存
  //const next = store.dispatch
  return function(next) { //直接返回一个函数,函数接收上一层的dispatch作为参数
    //之前的做法是直接用新函数替换
    //store.dispatch = function (action) { ... }
    return function (action) { 
      // ...logger功能
      return next(action)
  	}
  }
}
//改写成ES6的箭头函数
const logger = store => next => action => {
   // ...logger功能
  return next(action)
}

//error
const error = store => next => action => {
  //error功能
  return next(action)
}

上面说的loggererror就是我们所谓Redux中的中间件,中间件就是一个函数, 该函数接受一个dispatch函数作为参数,并返回一个新的dispatch函数, 返回的函数会被作为一下个中间件的next传入。我们也可以发现中间件实际上就是对原dispatch的功能进行增强,在action发出之后,reducer函数执行之前搞点事情,最终我们用于派发action的函数还是原Redux库提供的store.dispatch;

我之前这里一直有一个疑惑,为什么中间件函数需要有这么多层?第一层接收store,第二层接收next,第三层接收action,但中间件本质上处理的就是action对象,那为啥不直接把storenext放在同一层,然后返回一个函数,用于处理action呢?像下面这样

const logger = (store, next) => action => {
  consoe.log(store.getState(), action)
  return next(action)
}

这样不是更容易理解么?其实Redux是根据函数式编程的思想来写的,所以我在整理Redux知识的时候,先复习了一下函数式编程 ,Redux中用了好多函数式编程的思想来解决问题,而函数式编程一个重要的思想就是让函数的功能尽可能单一,然后再通过函数的嵌套组合来实现复杂的功能。

我们可以总结一下中间件的特点:中间件是一个独立的函数, 对store.dispatch方法进行增强或改造中间件可以组合使用,且结果跟中间件的执行顺序有一定关系中间件具有统一的接口,都是接收一个dispatch函数作为参数并返回一个新dispatch函数

applyMiddleWare

上面在介绍中间件时我们就说了,需要将中间件返回的dispatch函数,按照传入的顺序依次传入到下一个中间件,Redux库为我们提供了一个applyMiddleware方法;

function applyMiddleware(...middlewares) { //接收一些中间件作为参数
  return function (createStore) {
    return function () {
      var store = createStore.apply(void 0, arguments);

      //所有中间件功能都加载完,才能执行dispatch操作,否者就不完整了
      var _dispatch  = function dispatch() {
        throw new Error('Dispatching while constructing your middleware is not allowed. ' + 'Other middleware would not be applied to this dispatch.');
      };

      var middlewareAPI = {
        getState: store.getState,
        dispatch: function dispatch() {
          return _dispatch.apply(void 0, arguments);
        }
      };
      //遍历执行中间件,为中间件传入其内部所需要的一些变量,像logger内部用到的getState
      var chain = middlewares.map(function (middleware) {
        return middleware(middlewareAPI);
      });
      //可以这样认为,上一步的遍历执行是为了生成中间件,现在chain中的元素才是中间件本身,由于中间件本身是一个接收dispatch并返回一个新dispatch的函数;这里经compose复合函数 使中间件按照参数接收位置,依次从右往左执行,例如:compose(logger, error)  --> logger(error()),本身compose运行的结果就需要返回一个函数,而且最内层的error中间件,也需要传入一个dispatch函数,所以最后就变成了 dispatch => logger(error(dispatch))
      _dispatch = compose(...chain)(store.dispatch);
      //最终依然返回store, 只不过是对原有dispatch方法进行了增强
      return _objectSpread2({}, store, {
        dispatch: _dispatch
      });
    };
  };
}

//放上compose函数的源码,之前文章里有解释,这里不说啦
//注意一点: redux中 中间件不是按照传入的顺序执行, 而是从右往左执行
function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }
  if (funcs.length === 1) {
    return funcs[0];
  }
  return funcs.reduce((a, c) => (args) => a(c(args)))
  /**
    return funcs.reduce(function (a, c) {
      return function (args) {
        return a(c(args));
      };
    });
 */
}

异步操作

理解了上面说的中间件后,异步操作就有了解决办法,我们知道原本dispatch方法只能接收一个具有type属性的对象作为参数,但是对于我们自己扩展的增强型dispatch可以处理处理其他类型的参数:如果接收的action是对象类型直接放行,可以由Redux库默认的dispatch方法执行,对于像函数类型的异步操作,直接执行该函数,待异步操作执行完毕之后,将异步操作返回的结果包装成action对象,其巧妙之处就在于这个action函数, 我们可以事先将dispatch方法以参数的形式传入,再拿到异步操作结果后直接用传入的dispatch函数调用;

const reduxThunk = store => next => action => {
  if(typeof action === 'function') {
     return action(store.dispatch) //注意当action是函数类型时, 需要传入一个dispatch函数作为参数, 以便在异步操作结束时好发送dispatch请求
   }else {
     return next(action)
   }
}

createStore

虽然我们在上一篇文章中分析过createStore的源码,但那时候省略了第三个参数enhancer, 现在我们理解了中间件之后再回过头来看看添加enhancer之后的处理方式:

function createStore(reducer, preloadedState, enhancer) {
  //省略preloadedState时的逻辑判断,enhancer可以作为第二个参数传入
	if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
  }

  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }
    //我们的enhancer就是applyMiddleware(logger, error),这是一个函数调用表达式,它返回的结果是一个函数,而且需要接收createStore,用于内部生成store。
    // enhancer(createStore); 回到上面applyMiddleware,我们知道这里返回的也还是一个函数,需要接收reducer等参数,作为createStore函数的参数,生成store
    return enhancer(createStore)(reducer, preloadedState);
  }
 /**
 ...
 */
}