Redux系列之分析中间件原理(附经验分享)

落落落洛克 等级 311 0 0

我的前端学习笔记📒

最近花了点时间把笔记整理到语雀上了,方便童鞋们阅读

Redux系列之分析中间件原理(附经验分享)

Redux系列之分析中间件原理(附经验分享)

前提:公司兼容了两种技术栈VueReactVue研究的比较多一些,反观React还停留在查官方文档阶段,最近刚好维护了一个React项目,项目中用到Redux,借此重新复习Redux

Redux

一句话介绍Redux:Redux是一个可预测化的JavaScript状态管理容器。

三大原则

理解Redux离不开这三大原则

单一数据源

整个应用的 state 被储存在一棵 object tree 中,并且这个 object tree 只存在于唯一一个 store 中。

State 是只读的

唯一改变 state 的方法就是触发 action,action 是一个用于描述已发生事件的普通对象。

store.dispatch({
  type'COMPLETE_TODO',
  index1
})

使用纯函数来执行修改

为了描述 action 如何改变 state tree ,你需要编写 reducers。action是描述修改操作,而真正去操作修改state是reducers

function reducer(state = [], action{
  switch (action.type) {
    case 'ADD_TODO':
      return [
        ...state,
        {
          text: action.text,
          completedfalse
        }
      ]
    case 'COMPLETE_TODO':
      return state.map((todo, index) => {
        if (index === action.index) {
          return Object.assign({}, todo, {
            completedtrue
          })
        }
        return todo
      })
    default:
      return state
  }
}

通过上面👆的分析我们得到三个关键的信息点:state,action,reducers

概念

在复习工作流之前我们先来搞清楚几个概念,以下的概念解释都出自于词汇表 , 为了方便阅读,我就摘抄过来了

State

State (也称为 state tree) 是一个宽泛的概念,但是在 Redux API 中,通常是指一个唯一的 state 值,由 store 管理且由 getState() 方法获得。它表示了 Redux 应用的全部状态,通常为一个多层嵌套的对象。

约定俗成

顶层 state 或为一个对象,或像 Map 那样的键-值集合,也可以是任意的数据类型。然而你应尽可能确保 state 可以被序列化,而且不要把什么数据都放进去,导致无法轻松地把 state 转换成 JSON。

Action

Action 是一个普通对象,用来表示即将改变 state 的意图。它是将数据放入 store 的唯一途径。无论是从 UI 事件、网络回调,还是其他诸如 WebSocket 之类的数据源所获得的数据,最终都会被 dispatch 成 action。

约定俗成

action 必须拥有一个 type 域,它指明了需要被执行的 action type。Type 可以被定义为常量,然后从其他 module 导入。比起用 Symbols 表示 type,使用 String 是更好的方法,因为 string 可以被序列化。

Reducer

Reducer (也称为 reducing function) 函数接受两个参数:之前累积运算的结果和当前被累积的值,返回的是一个新的累积结果。该函数把一个集合归并成一个单值。

在 Redux 中,累计运算的结果是 state 对象,而被累积的值是 action。Reducer 由上次累积的结果 state 与当前被累积的 action 计算得到一个新 state。这些 Reducer 必须是纯函数,而且当输入相同时返回的结果也会相同。它们不应该产生任何副作用。正因如此,才使得诸如热重载和时间旅行这些很棒的功能成为可能。

dispatch 函数

dispatching function(或简言之 dispatch function) 是一个接收 action 或者异步 action的函数,该函数要么往 store 分发一个或多个 action,要么不分发任何 action。

Action Creator

Action Creator 很简单,就是一个创建 action 的函数。不要混淆 action 和 action creator 这两个概念。Action 是一个信息的负载,而 action creator 是一个创建 action 的工厂

异步 Action

异步 action 是一个发给 dispatching 函数的值,但是这个值还不能被 reducer 消费。在发往 base dispatch() function 之前,middleware 会把异步 action 转换成一个或一组 action。异步 action 可以有多种 type这取决于你所使用的 middleware。它通常是 Promise 或者 thunk 之类的异步原生数据类型虽然不会立即把数据传递给 reducer,但是一旦操作完成就会触发 action 的分发事件

Middleware

Middleware 是一个组合 dispatch function 的高阶函数返回一个新的 dispatch function,通常将异步 actions 转换成 action。

❗️❗️这也是接下来我们要重点分析

最后放出一张记忆脑图

>Redux系列之分析中间件原理(附经验分享)

Redux工作流

了解了三大原则以及概念以后,来看看Redux的工作流吧

Redux系列之分析中间件原理(附经验分享)

对比下图就能轻松理解了

Redux系列之分析中间件原理(附经验分享)

从图中我们知道Redux是单向数据流,那么根据上面所学的知识我们来设计下我们的Redux目录结构

Redux系列之分析中间件原理(附经验分享)

如上图,大部分公司的Redux目录结构应该类似这样,我们需要actionCreators文件来创建我们的actionaction对象必须拥有一个 type 域,然后reducer根据不同的type触发对应的操作,所以创建actionTypes文件,接下来就是处理reducer了来修改我们的state,所以创建reducer文件📃,所以明白上述概念,有助于我们对目录结构的理解,而不是傻乎乎的跟着别人的目录结构照猫画虎的创建,起码要明白为什么这样划分

compose聚合函数

再看中间件原理时,我们来实现下compose函数,理解它对于理解我们中间件原理有很大帮助

dispatch=fn1(fn2(fn3))
dispatch=compose(fn1,fn2,fn3)

我们期待有一个聚合的方法compose,可以这样使用,参数从右至左,将第一个参数fn3作为第二个参数fn2的参数,并将运行结果作为第三个参数fn1的参数,依次递推,最终返回一个新的函数,这个新函数在基础函数f3的基础上,得到了所有的高阶函数的能力,🤔思考:假设这个f3参数是换成是dispatch函数呢?,不着急,我们接着往下分析compose函数的实现

// compose聚合函数的顺序是从右到左 from right to left
const compose = function (...funcs{
    return funcs.reduce((a, b) => {
        return (...args) => {
           return a(b(...args))
        }
    })
}

这些串联函数不优雅。ES6 的箭头函数简写 ,从而看起来更舒服一些

 function compose(...funcs{
  return funcs.reduce((a, b) => (...args) => a(b(...args)))
}

所以compose(fn1, fn2, fn3) (...args) 相当于 fn1(fn2(fn3(...args)))

Redux中间件原理

讲清楚了上述👆基本内容后,到了本问的关键点了,那就是Redux中间件是用来干嘛的,原理是什么❓

前面分析Redux工作流Action发出以后,Reducer 立即算出 State,是个同步流程,那么想想如何支持异步操作,不单单支持异步操作,还要支持错误处理、日志监控,那么是在Redux工作流哪个环节进行拦截操作呢❓,答案是dispatch过程,在分发action进行拦截处理

在Redux中,与中间件的实现相关联的方法是applyMiddleware,所以我们来分析下这个方法吧(这里笔者提供一份调试中间件代码点击进入仓库

// 调用applyMiddleware
applyMiddleware(thunk, logger)

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.'
      )
    }

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

可以看到dispatch = compose(...chain)(store.dispatch)这行关键代码,Redux为了支持中间件,内部重写了原来的dispatch方法,同时最后传入原来的store.dispatch方法,也就是将原来的disptach方法也当做中间件处理了

分析到这里,我们可以知道传入的中间件compose函数聚合后改写dispatch方法,所以我们也可以理解成中间件是对dispatch方法的增强,比如说:增强dispatch函数对action是函数、Promise的处理

举个例子🌰

仓库内修改成这段代码

function logger(store{
    return function wrapDispatchToAddLogging(next{
        return function dispatchAndLog(action{
            let result = next(action)
            return result
        }
    }
}

function thunk({ dispatch, getState }{
    return function wrapDispatchToThunk(next{
        return function dispatchThunk(action{
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            return next(action);
        }
    }
}

applyMiddleware(thunk, logger)  // 相当于 wrapDispatchToThunk(wrapDispatchToAddLogging(dispatch))  

打印dispatch方法

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

转换next

// 转换next
dispatchThunk(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            // next(action)
            // 这里的next由来是执行logger方法返回了dispatchAndLog函数
            return (function dispatchAndLog(action{
            let result = next(action)
            return result
        })(action)
   }   

继续转换next

看到dispatchAndLog函数里还有个next,我们继续转换

// 转换next
dispatchThunk(action) {
            if (typeof action === 'function') {
                return action(dispatch, getState);
            }
            // next(action)
            // 这里的next由来是执行logger方法返回了dispatchAndLog函数
            return (function dispatchAndLog(action{
            // let result = next(action)
           // 这里的dispatch是原来的dispacth
            let result=(function(action){dispatch(action)})(action)             
            return result
        })(action)
   }   

看到这里我们已经知道,Redux实现中间件的原理核心是增强原来dispatch函数的能力,然函数拥有某种能力自然而然想到高阶函数的处理方式,compose 方法将新的 middlewares 和 store.dispatch 结合起来,生成一个新的 dispatch 方法,另外通过改写后的dispatch方法,可以确定Redux中间件也是基于洋葱模型

中间件的执行顺序

执行顺序遵从洋葱模型

  applyMiddleware( 
    logger,
    thunk
  )
Redux系列之分析中间件原理(附经验分享)

如何写Redux中间件

工作中我们明白了Redux工作流的情况下,其实干扰最多的可能就是Redux中间件了,常用的有redux-thunk、redux-soga、redux-promise等等,所以掌握中间件原理还是很重要的,那么如何去写一个中间件❓,我们通过继续分析applyMiddleware方法

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.'
      )
    }

    const middlewareAPI = {
      getState: store.getState,
      dispatch(...args) => {
        return dispatch(...args)
      }
    }
    // 这里执行了一层中间件接收了{store.getState,dispatch}参数
    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    // compose(...chain)(store.dispatch) 相当于fn1(fn2(fn3(store.dispatch)))
    // 又执行了一层中间件 这一层接收next参数 也就是下一个中间件参数
    dispatch = compose(...chain)(store.dispatch)
    return {
      ...store,
      dispatch
    }
  }
}

通过上面👆的分析可知,一个Redux中间件的基本形式(结构)如下

// 中间件逻辑代码需要经过三次柯里化
store => next => action => {
  // 中间件逻辑代码
}

Redux支持异步操作

  • 强化dispacth函数,让其能解析action为函数形式,从而让Redux支持异步操作
  • 强化dispacth函数,让其能解析action为Promise对象形式,从而让Redux支持异步操作
redux-thunk

根据这个中间件结构我们来分析redux-thunk中间件的源码

function createThunkMiddleware(extraArgument{
  return ({ dispatch, getState }) => (next) => (action) => {
    // 如果是函数 thunk来处理
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }
    // 其它处理不了,交给下一个中间件处理
    return next(action);
  };
}
const thunk = createThunkMiddleware();

嗯嗯…,redux-thunk就这么几行源代码就实现了支持Redux异步操作

redux-promise

用法

  const testRes = () => {
      return new Promise((res, rej) => {
          res({
              type'TEST_RESOLVE'
          })
      });
  }
  store.dispatch(testRes());
// redux-promise简易版源码
const vanillaPromise = store => next => action => {
  // 判断不是Promise对象,交给下个中间件处理
  if (typeof action.then !== 'function') {
    return next(action)
  }
  // action为Promise对象,promise中间件能做处理
  // 最后异步执行完触发执行store.dispatch ---> (...args) =>  dispatch(...args)

  return Promise.resolve(action).then(store.dispatch)
}

改造我们的Redux项目

一句话Redux用起来太笨重了,去年实习期开始写React项目时候,跟着别人的风格来写而已,直到维护同事的React项目,才猛然意识到项目中糟糕Redux写法,滥用Redux,导致了这个项目是灾难级别的

后面意识到能不能二次封装下Redux,简化写法

比如:

import { createModel } from "../../../model.js";

const model = {
  namespace'counter',
  state: {
    count10
  },
  reducer: {
    add(state: any, action: any) { // counter/add
      state.count += 1
    },
    minus(state: any, action: any) {
      state.count--
    },
  }
}

export default createModel(model)
Redux系列之分析中间件原理(附经验分享)

enennene….就有了以上对话,也感谢大佬的帮助,最后选用了remacth方案,这个库也挺好友好,兼容了老的写法,具体可以细看这个它的文档,这里就不做分析了

## 我的前端学习笔记📒 最近花了点时间把笔记`整理到语雀上`了,方便`童鞋们阅读`

Redux系列之分析中间件原理(附经验分享)

Redux系列之分析中间件原理(附经验分享)

收藏
评论区