前言

这几天看了redux middleware的运用与实现原理,写了一个百度搜索的demo,实现了类似redux-thunk和redux-logger中间件的功能。

项目地址:https://github.com/CanFoo/react-baidu-search/tree/master

redux中间件是通过函数式编程实现,因此要阅读源码需要有一定函数式编程基础,比如柯里化函数的实现,否则难以理解源码的缘由。接下去通过这个demo给大家讲解个人对中间件的理解,如有问题,come on 指正。

redux middleware是什么

如果没有中间件的运用,redux 的工作流程是这样 action -> reducer,这是相当于同步操作,由dispatch 触发action后,直接去reducer执行相应的动作。但是在某些比较复杂的业务逻辑中,这种同步的实现方式并不能很好的解决我们的问题。比如我们有一个这样的需求,点击按钮 -> 获取服务器数据 -> 渲染视图,因为获取服务器数据是需要异步实现,所以这时候我就需要引入中间件改变redux同步执行的流程,形成异步流程来实现我们所要的逻辑,有了中间件,redux 的工作流程就变成这样 action -> middlewares -> reducer,点击按钮就相当于dispatch 触发action,接下去获取服务器数据 middlewares 的执行,当 middlewares 成功获取到服务器就去触发reducer对应的动作,更新需要渲染视图的数据。中间件的机制可以让我们改变数据流,实现如异步 action ,action 过滤,日志输出,异常报告等功能。

如何自定义中间件

redux 提供了一个叫 applyMiddleware 的方法,可以应用多个中间件,这样就可以当触发action时候就会被这些中间件给捕获,这边我们分别定义了搜索和日志的中间件,而且中间件传入顺序是先搜索中间件再日志打印中间件,这个顺序是有讲究的,下文会说明的

import { createStore } from 'redux'
import applyMiddleware from './applyMiddleware/applyMiddleware'
import compose from './applyMiddleware/compose'
import reducer from './reducers'
import loggerMiddleware from './middlewares/loggerMiddleware'
import searchMiddleware from './middlewares/searchMiddleware' const createStoreWithMiddleware = compose(
applyMiddleware(
searchMiddleware,
loggerMiddleware
),
window.devToolsExtension ? window.devToolsExtension() : f => f
)(createStore)

searchMiddleware是搜索中间件,其实它就是仿照redux-thunk的实现

export default function thunkMiddleware({ dispatch, getState }) {
return next => action => {
/*如果action是一个函数,则先执行action,否则通过next进入到下一个action对应的reducer*/
typeof action === 'function' ?
action(dispatch, getState) : //这里action(dispatch, getState)指的是getThenShow返回函数的执行
next(action);
}}

我们先来分析下中间件的执行顺序,下文再来解释为什么这里结构是() => next => action => {}以及源码是如何实现中间件链。这里的代码非常简单,就是一个三目运算符,当action是一个函数类型,就直接执行这个action函数,否则就通过next来链接到下一个组件,next是专门用来串联组件间的执行顺序。我们知道,如果没有中间件的处理,action只能返回一个对象格式,否则reducer不能进行处理action传过来的行为,但是有了中间件,我们就可以“肆意妄为”,只要保证最终传给reducer的action是一个对象,期间从触发action到真正到达reducer我想要action是什么就可以是什么,下面是搜索逻辑所对应的action

export function getThenShow(value) {
return dispatch => {
const url = 'https://gsp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su';
jsonp(url, {wd: value}, 'cb', (data) => {
dispatch({
type: SHOW_MESSAGE_SUCESS,
lists: data.s
});
})
}
}

当执行dispatch(getThenShow())时,上文所说的searchMiddleware就会捕获到这个action,因为第一次传过来的action是

functon(dispatch) {
const url = 'https://gsp0.baidu.com/5a1Fazu8AA54nxGko9WTAnF6hhy/su';
jsonp(url, {wd: value}, 'cb', (data) => {
dispatch({
type: SHOW_MESSAGE_SUCESS,
lists: data.s
});
})

如上所示,因为第一次传过来的action是function类型的,所以searchMiddleware里就会先执行这个action函数,这个aciton执行jsonp方法回调另一个dispatch,从上面代码可以看出这个时候dispatch分发的action是一个对象,type为SHOW_MESSAGE_SUCCESS,lists为搜索结果,因此下一次searchMiddleware捕获到的action为一个对象,从而去执行next(action),也就是去执行下一个中间件loggerMiddleware的内容

export default function createLogger({ getState }) {
return (next) => (action) => {
const prevState = getState();
const returnValue = next(action);
const nextState = getState();
console.log(`%c prev state`, `color: #9E9E9E`, prevState);
console.log(`%c action`, `color: #03A9F4`, action);
console.log(`%c next state`, `color: #4CAF50`, nextState);
console.log('===============')
return returnValue;
};
}

loggerMiddleware里先通过getState()获取到当前状态,接着用通过next(action)执行下一个中间件,由于applyMiddleware传入中间件的顺序是先searchMiddleware再loggerMiddleware,而且loggerMiddleware之后没有其它中间件传入,因此此时的next指的是原生的dispatch,进而会去触发reducer所对应的动作,所以再次调用getSatate()会返回下一个状态内容。

所以当在搜索框输入内容redux执行的步骤为:

1.输入内容触发dispatch(getThenShow())发起第一个action(这里getThenShow()返回的action的是一个函数)

2.searchMiddleware捕获到第一个action,因为action返回是一个函数,所以执行action

3.第一个action执行完后回调再次触发dispatch({type: SHOW_MESSAGE_SUCCESS, lists: ...})发起第二个action

4.searchMiddleware捕获到第二个action,执行next(action),链接到loggerMiddleware

5.loggerMiddleware获取当前(prev)状态后执行next(action)从而触发reducer的对应的动作

6.loggerMiddleware再次获取当前(next)状态,然后打印出状态,执行完loggerMiddleware程序

7.回溯到searchMiddleware,searchMiddleware程序执行完

8.整个redux中间件执行完

到这里,我们就把这个demo的中间件执行顺序分析完,那么为什么自定义中间件是() => next => action => {}这样的结构?next又是如何将中间件串联起来的?其实这个两个问题是同一个问题,因为中间件设定这样() => next => action => {}的结构目的就是为了把中间件串联起来的。为了一探究竟,我们还是得来来看看applyMiddleware源码的实现

applyMiddleware 源码分析

export default function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState, enhancer) => {
var store = createStore(reducer, preloadedState, enhancer)
var dispatch = store.dispatch
var chain = [] var middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
}
chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)
return {
...store,
dispatch
}
}
}

从applyMiddleware执行完后最终返回的也是一个闭包函数,将创建 store的步骤放在这个闭包内执行,这样中间件就可以共享 store 对象。applyMiddleware是这样来对传进来中间件进行函数式编程处理的

1.通过...middlewares将所有的中间件存入到middlewares数组中

2.middlewares 数组通过 map 方法执行生成新的 middlewares 数组且每一项都传入middlewareAPI,传入middlewareAPI的目的就使得每个中间件都可以访问到store,这时候middlewares数组的每一项都变为了

function (next) {
return function (action) {...}
}

3.compose 方法将新的 middlewares 和 store.dispatch 结合起来,生成一个新的 dispatch 方法。这里的关键点是compose,我们来看看compose的设计

export default function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg
} if (funcs.length === 1) {
return funcs[0]
} const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
const fn = (...args) => rest.reduceRight((composed, f) => f(composed), last(...args))
return fn
}

可以看到 compose 方法实际上就是利用Array.prototype.reduceRight来进行下一步处理的。如果对reduceRight方法不了解的童鞋,可以先看看这里。我们这里可以来模拟一下compose函数处理完的结果,假设我们这边有两个中间件A和B,则传入到compose的func为[A, B],且A、B的形式已经是(next) => (action) => {}

function A(next) {
console.log('A...next === ', next)
return function(action) {
console.log('A...action')
next(action)
}
}
function B(next) {
console.log('B...next === ', next)
return function(action) {
console.log('B...action')
next(action)
}
} function compose(funcs) {
if (funcs.length === 0) {
return arg => arg
} if (funcs.length === 1) {
return funcs[0]
}
const last = funcs[funcs.length - 1]
const rest = funcs.slice(0, -1)
const fn = (args) => rest.reduceRight((composed, f) => f(composed), last(args))
return fn
} var fnArr = [A, B]
var dispatch = compose(fnArr)("store.dispatch")
console.log('new dispatch === ', dispatch)

执行的结果是

由结果可以看到中间件A的next是指向中间件B的最内层闭包函数,而中间件B的next则是指向原生的dispatch,所以通过compose执行完后,所有的中间件就通过next串联起来了。这也就是为什么我们所分析这个百度搜索demo中的searchMiddleware的next是指向loggerMiddleware,而loggerMiddleware的next指向原生dispatch的原因。

4.返回的 store 新增了一个 dispatch 方法, 这个新的 dispatch 方法是改装过的 dispatch,由上例中这个改装过的 dispatch就是指的是中间件A最里层的闭包函数,这也就是为什么说有了中间件就可以捕获action的行为的原理。

到此applyMiddleware源码分析完毕,我们也可以明白为什么自定义组件需要设计成() => next => action => {}的形式,其实也就是设计成柯里化方式,因为这样方便进行compose,从而达到动态产生 next 方法以及保持 store 的一致性。


教程源代码地址

https://github.com/CanFoo/react-baidu-search/tree/master

redux middleware 的理解的更多相关文章

  1. 如何学习理解Redux Middleware

    Redux中的middleware其实就像是给你提供一个在action发出到实际reducer执行之前处理一些事情的机会.可以允许我们添加自己的逻辑在这段当中.它提供的是位于 action 被发起之后 ...

  2. Redux Middleware All in One

    Redux Middleware All in One https://redux.js.org/advanced/middleware https://redux.js.org/api/applym ...

  3. 再探Redux Middleware

    前言 在初步了解Redux中间件演变过程之后,继续研究Redux如何将中间件结合.上次将中间件与redux硬结合在一起确实有些难看,现在就一起看看Redux如何加持中间件. 中间件执行过程 希望借助图 ...

  4. 初识Redux Middleware

    前言 原先改变store是通过dispatch(action) = > reducer:那Redux的Middleware是什么呢?就是dispatch(action) = > reduc ...

  5. koa/redux middleware系统解析

    middleware 对于现有的一些框架比如koa,express,redux,都需要对数据流进行一些处理,比如koa,express的请求数据处理,包括json.stringify,logger,或 ...

  6. redux middleware 源码分析

    原文链接 middleware 的由来 在业务中需要打印每一个 action 信息来调试,又或者希望 dispatch 或 reducer 拥有异步请求的功能.面对这些场景时,一个个修改 dispat ...

  7. 对redux的粗略理解

    redux是一个js库,用于前端应用的状态管理,但是在一个较小的项目中,即一个并不需要太多交互的项目中完全可以不用redux,非要使用的话反而增加了项目的复杂度. 关于redux就是状态与数据一一对应 ...

  8. koa/redux middleware 深入解析

    middleware 对于现有的一些框架比如koa,express,redux,都需要对数据流进行一些处理,比如koa,express的请求数据处理,包括json.stringify,logger,或 ...

  9. [React + Functional Programming ADT] Create Redux Middleware to Dispatch Actions with the Async ADT

    We would like the ability to group a series of actions to be dispatched with single dispatching func ...

随机推荐

  1. 变通实现微服务的per request以提高IO效率(二)

    *:first-child { margin-top: 0 !important; } body>*:last-child { margin-bottom: 0 !important; } /* ...

  2. JS时间戳格式化日期时间

    由于mysql数据库里面存储时间存的是时间戳,取出来之后,JS要格式化一下显示.(李昌辉) 用的次数比较多,所以写了一个简单方法来转换: //时间戳转时间 function RiQi(sj) { va ...

  3. .net 实体类与json转换(.net自带类库实现)更新

    上一篇文章中写到在.net中实体类跟json格式的相互转换,今天在做具体转换时候,发现之前版本的jsonhelp对于日期类型的转换不全面.之前版本的jsonhelp中从实体类转换成json格式时候,将 ...

  4. webpack CommonsChunkPlugin详细教程

    1.demo结构: 2.package.json配置: { "name": "webpack-simple-demo", "version" ...

  5. eclipse启动的时候报错 出现Java was started but returned exit code=13

    eclipse启动的时候出现 这是你的jdk环境与你eclipse版本不匹配, 如果你的eclipse是32位的 jdk也得是32位的   重新安装一个比配的jdk就好了 如果你的jdk是解压版的   ...

  6. Android 窗体设置

    requestWindowFeature(Window.FEATURE_NO_TITLE);  getWindow().setFlags(WindowManager.LayoutParams.FLAG ...

  7. android SQLite 批量插入数据慢的解决方案 (针对于不同的android api 版本)

    原地址 :http://www.cnblogs.com/wangmars/p/3914090.html SQLite,是一款轻型的数据库,被广泛的运用到很多嵌入式的产品中,因为占用的资源非常少,二其中 ...

  8. IOS 杂笔-16 (-(void)scrollViewDidEndScrollingAnimation:方法使用注意)

    今天在写项目的时候,遇到了一件令人抓狂的事情. 正如标题所示,被这个方法弄的团团转. -(void)scrollViewDidEndScrollingAnimation:是协议里的方法. 意味当动画结 ...

  9. Ignite安装配置——中篇

    Linux Ignite配置——上篇大体介绍了一下Ignite工具的功能.特性等,以及如何在Linux 上安装配置.从上篇可见Ignite安装非常的简单方便.下面介绍一下Ignite Reposito ...

  10. 小心SQL SERVER 2014新特性——基数评估引起一些性能问题

    在前阵子写的一篇博文"SQL SERVER 2014 下IF EXITS 居然引起执行计划变更的案例分享"里介绍了数据库从SQL SERVER 2005升级到 SQL SERVER ...