React学习笔记——redux里中间件Middleware的运行机理
1、前言
上篇文章中,我们详细介绍了redux的相关知识和如何使用,最后使用中间件Middleware来帮助我们完成异步操作,如下图
上面是很典型的一次 redux 的数据流的过程,在增加了 middleware 后,我们就可以在这途中对 action 进行截获,并进行改变,进行其他操作。
同时,在使用 middleware 时,我们可以通过串联不同的 middleware
来满足日常的开发,每一个 middleware 都可以处理一个相对独立的业务需求且相互串联。
如上图所示,派发给 redux Store 的 action 对象,会被 Store 上的多个中间件依次处理,如果把 action 和当前的 state 交给 reducer 处理的过程看做默认存在的中间件,那么其实所有的对 action 的处理都可以有中间件组成的。值得注意的是这些中间件
会按照指定的顺序一次处理
传入的 action,只有排在前面的中间件完成任务之后,后面的中间件才有机会继续处理 action
,同样的,每个中间件都有自己的“熔断”处理
,当它认为这个 action 不需要后面的中间件进行处理时,后面的中间件也就不能再对这个 action 进行处理了
下面我们来研究研究Middleware。
2、正文
2.1、redux-thunk源码
我们以redux-thunk
为例,从node_modules文件夹下面找到redux-thunk文件夹,查看其源码(下图为redux-thunk源码,一共12行)
function createThunkMiddleware(extraArgument) {return ({ dispatch, getState }) => next => action => {if (typeof action === 'function') {return action(dispatch, getState, extraArgument);}return next(action);};
}
const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
可以看出,thunk是createThunkMiddleware()运行的结果,而该函数里面还包裹了3层函数(柯里化),函数一层一层向下执行。
我们将其中的ES6的箭头函数换成普通函数,再观察
function createThunkMiddleware (extraArgument){// 第一层/* getState 可以返回最新的应用 store 数据 */return function ({dispatch, getState}){// 第二层/* next 表示执行后续的中间件,中间件有可能有多个 */return function (next){// 第三层/*中间件处理函数,参数为当前执行的 action */return function (action){if (typeof action === 'function'){return action(dispatch, getState, extraArgument);}return next(action);};}}
}
let thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;
export default thunk;
- 首先是
外层
,从thunk最后两行源码可知,这一层存在的主要目的是支持在调用applyMiddleware并传入thunk的时候可以不直接传入thunk本身
,而是先调用包裹了thunk的函数
(第一层柯里化的父函数),并传入需要的额外参数
,再将该函数调用的后返回的值
(也就是真正的thunk)传给applyMiddleware
,从而实现对额外参数
传入的支持,使用方式如下:
const store = createStore(reducer, applyMiddleware(thunk.withExtraArgument({api, whatever})));
- 如果
无需额外参数
则用法如下:
const store = createStore(reducer, applyMiddleware(thunk));
- 接着看
第一层
,这一层是真正applyMiddleware能够调用的一层,从形参
来看,这个函数接收了一个类似于store
的对象,因为这个对象被结构
以后获取
了它的dispatch
和getState
这两个方法,巧的是store也有这两方法,但这个对象到底是不是store,还是只借用了store的这两方法合成的一个新对象?这个问题在我们后面分析applyMiddleware源码时,自会有分晓 - 再来看
第二层
,我们接收的一个名为next
的参数,并在第三层函数
内的最后一行代码中用它去调用了一个action对象
,感觉有点 dispatch({type: 'XX_ACTION', data: {}}) 的意
思,因为我们可以怀疑它就是一个dispatch方法,或者说是其他中间件处理过的dispatch方法,似乎能通过这行代码链接上所有的中间件,并在所有只能中间件自身逻辑处理完成后,最终调用真实的store.dispath去dispatch一个action对象,再走到下一步,也就是reducer内 - 最后看
第三层
,在这一层函数的内部源码中首先判断了action的类型
:如果action是一个方法
,我们就调用它
,并传入dispatch、getState、extraArgument三个参数
,因为在这个方法内部,我们可能需要调用到这些参数,至少dispatch是必须的。这三行源码才是真正的thunk核心所在,简直是太简单了。所有中间件的自身功能逻辑也是在这里实现的。如果action不是一个函数
,就走之前解析第二层
时提到的步骤。
2.2、ApplyMiddleware源码
applyMiddleware函数共十来行代码,这里将其完整复制出来。
import compose from './compose'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) => dispatch(...args),}// 1、将store对象的基本方法传递给中间件并依次调用中间件const chain = middlewares.map((middleware) => middleware(middlewareAPI))// 2、改变dispatch指向,并将最初的dispatch传递给composedispatch = compose(...chain)(store.dispatch)return {...store,dispatch,}}
}
同样,我们将applyMiddleware的ES6箭头函数形式转换成ES5普通函数的形式
function applyMiddleware (...middlewares){return function (createStore){return function (reducer, preloadedState, enhancer){const store = createStore(reducer, preloadedState, enhancer);let dispatch = function (){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) => dispatch(...args)};// 1、将store对象的基本方法传递给中间件并依次调用中间件const chain = middlewares.map(middleware => middleware(middlewareAPI));// 2、改变dispatch指向,并将最初的dispatch传递给composedispatch = compose(...chain)(store.dispatch);return {...store,dispatch};}}
}
从其源码可以看出,applyMiddleware内部一开始也是两层柯里化
,所以我们看看和applyMiddleware最有关系的createStore的主要源码。
2.3、CreateStore源码
在平时业务中,我们创建store时,一般这样写
const store = createStore(reducer,initial_state,applyMiddleware(···));
或者
const store = createStore(reducer, applyMiddleware(...));
所以我们也要关注createStore
和applyMiddleware
的源码
createStore部分源码:
// 摘至createStore
export function createStore(reducer, preloadedState, enhancer) {...if (typeof enhancer !== 'undefined') {if (typeof enhancer !== 'function') {throw new Error('Expected the enhancer to be a function.')}/*若使用中间件,这里 enhancer 即为 applyMiddleware()若有enhance,直接返回一个增强的createStore方法,可以类比成react的高阶函数*/return enhancer(createStore)(reducer, preloadedState)}............dispatch({ type: ActionTypes.INIT })return {dispatch,subscribe,getState,replaceReducer,[$$observable]: observable,}
}
对于createStore的源码我们只需要关注和applyMiddleware有关
的地方。从其内部前面一部分代码来看,其实很简单,就是对调用createStore时传入的参数进行一个判断
,并对参数做矫正
,再决定以哪种方式来执行后续代码
。据此可以得出createStore有多种使用方法,根据第一段参数判断规则,我们可以得出createStore的两种使用方式:
const store = createStore(reducer, {a: 1, b: 2}, applyMiddleware(...));
以及
const store = createStore(reducer, applyMiddleware(...));
- 根据第一段参数判断规则,我们可以肯定的是:
applyMiddleware返回的一定是一个函数
- 经过createStore中的第一个参数判断规则后,对参数进行了校正,得到了新的enhancer得值:如果
新的enhancer
的值不为undeifined
,便将createStore传入enhancer
(即applyMiddleware调用后返回的函数)内,让enhancer执行创建store的过程
。也就时说这里的:
enhancer(createStore)(reducer, preloadedState);
实际上等同于:
applyMiddleware(mdw1, mdw2, mdw3)(createStore)(reducer, preloadedState);
这也解释了为啥applyMiddleware会有两层柯里化
,同时表明它还有一种很函数式编程的用法,即 :
const store = applyMiddleware(mdw1, mdw2, mdw3)(createStore);
这种方式将创建store
的步骤完全放在了applyMiddleware内部
,并在其内第二层柯里化
的函数内执行创建store的过程即调用createStore
,调用后程序将跳转至createStore
走参数判断流程最后再创建store
。
无论哪一种执行createStore的方式,我们都终将得到store,也就是在creaeStore内部最后返回的那个包含dispatch
、subscribe
、getState
等方法的对象。
2.4、回看ApplyMiddleware源码
对于applyMiddleware开头的两层柯里化的出现原因以及和createStore有关的方面,在前面分析过。同时,我们之前在redux-thunk里的第一层柯里化中猜测传入的对象是一个类似于store的对象,通过上个章节中applyMiddleware的确实可以确认了。
这里我们主要讨论中间件是如何通过applyMiddleware的工作起来并实现挨个串联的。
接下来这几段代码是整个applyMiddleware的核心部分
,也解释了在第二章节中,我们对thunk中间件为啥有三层柯里化的疑虑
// ...
// 1、将store对象的基本方法传递给中间件并依次调用中间件
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 2、改变dispatch指向,并将最初的dispatch传递给compose
dispatch = compose(...chain)(store.dispatch);return {...store,dispatch
};
// ...
- 首先,我们可以直观的看到,applyMiddleware的执行结果最终返回的是:
store的所有方法
和一个dispatch方法
。
2.4.1、redux-thunk的第一层柯里化
这个dispatch方法是怎么来的呢?我们来看头两行代码,这两行代码也是所有中间件被串联起来的核心部分实现,它们也决定了中间件内部为啥会有我们在之前章节中提到的三层柯里化的固定格式,先看第一行代码:
const chain = middlewares.map(middleware => middleware(middlewareAPI));
- 遍历所有的中间件,并调用它们,传入那个类似于store的对象middlewareAPI,这会
导致
中间件(redux-thunk)中第一层柯里化函数被调用
,并返回
一个接收next(即dispatch)方法作为参数的新函数
- 这一层柯里化主要原因,还是考虑到中间件内部会有调用store方法的需求,所以我们需要在此注入相关的方法,其内存函数可以通过闭包的方式来获取并调用,若有需要的话
- 遍历结束以后,我们拿到了一个
包含所有中间件
新返回的函数的一个数组
,将其赋值给变量chain
,译为函数链
2.4.2、redux-thunk的第二层柯里化
再来看第二句代码:
dispatch = compose(...chain)(store.dispatch);
- 我们展开了这个数组,并将其内部的元素(函数)传给了compose函数,compose函数又返回了我们一个新函数。然后我们再调用这个新函数并传入了原始的未经任何修改的dispatch方法,最后返回一个经过了修改的新的dispatch方法
- 先说一句,
compose是从右到左依次调用传入其内部的函数链
- thunk中间件的
第二层柯里化函数即在compose内部被调用
,并接收了经其右边那个中间函数改造并返回dispatch方法作为入参,并返回一个新的函数,再在该函数内部添加自己的逻辑,最后调用右边那个中间函数改造
并返回dispatch方法
接着执行前一个中间件
的逻辑(当然如果只有一个thunk中间件被应用了,或者他出入传入compose时的最后一个中间件,那么传入的dispatch方法即为原始的store.dispatch方法)
2.4.3、redux-thunk的第三层柯里化
thunk的第三层柯里化函数,即为被thunk改造后的dispatch方法:
// ...
return function (action){// thunk的内部逻辑if (typeof action === 'function'){return action(dispatch, getState, extraArgument);}// 调用经下一个中间件(在compose中为之前的中间件)改造后的dispatch方法(本层洋葱壳的下一层),并传入actionreturn next(action);
};
// ...
- 这个改造后的dispatch函数将通过compose传入thunk左边的那个中间件作为入参
2.4.4、总结
经上述分析,我们可以得出一个中间件的串联和执行时的流程,以下面这段使用applyMiddleware的代码为例:
export default createStore(reducer, applyMiddleware(middleware1, middleware2, middleware3));
- 在applyMiddlware内部的compose
串联中间件
时,顺序是从右至左
,就是先调用middleware3、再middleware2、最后middleware1 - middleware3最开始接收真正的store.dispatch作为入参,并返回改造的的dispatch函数作为入参传给middleware2,这个改造后的函数内部包含有对原始store.dispatch的调用。依次内推知道从右到左走完所有的中间件
- 整个过程就像是给原始的store.dispatch方法套上了一层又一层的壳子,最后得到了一个类似于
洋葱结构
的东西,也就是下面源码中的dispatch,这个经过中间件改造
并返回的dispatch方法将替换store被展开后的原始的dispatch方法
:
// ...
return {...store,dispatch
};
// ...
- 而
原始的store.dispatch
就像这洋葱内部的芯
,被覆盖在了一层又一层的壳的最里面 - 而当我们剥壳的时候,剥一层壳,执行一层的逻辑,即走一层中间件的功能,直至调用藏在
最里边的原始的store.dispatch方法去派发action
。这样一来我们就不需要在每次派发action的时候再写单独的代码逻辑的
如上图所示:
- 在
中间件串联
的时候,middleware1-3
的串联顺序
是从右至左
的,也就是middleware3
被包裹在了最里面
,它内部含有对原始的store.dispatch的调用
,middleware1
被包裹在了最外边
- 在
执行业务代码中dispatch一个action
时,也就是中间件执行
的时候,middleware1-3
的执行顺序
是从左至右
的,因为最后被包裹的中间件,将被最先执行
2.5、总体流程
进过上述分析,我们可以将其主要功能按步骤划分如下:
1、依次执行middleware:
将middleware
执行后返回的函数合并
到一个chain数组
,这里我们有必要看看标准middleware的定义格式,如下
**加粗样式**export default store => next => action => {}// 即
function (store) {return function(next) {return function (action) {return {}}}
}
那么此时合并的chain结构如下
[ ...,function(next) {return function (action) {return {}}}
]
2、改变dispatch指向:
想必你也注意到了compose函数
,compose函数如下:
[...chain].reduce((a, b) => (...args) => a(b(...args)))
实际就是一个柯里化函数
,即将所有的middleware合并成一个middleware
,并在最后一个middleware中传入当前的dispatch
。
// 假设chain如下:
chain = [a: next => action => { console.log('第1层中间件') return next(action) }b: next => action => { console.log('第2层中间件') return next(action) }c: next => action => { console.log('根dispatch') return next(action) }
]
调用compose(...chain)(store.dispatch)
后返回a(b(c(dispatch)))
。
可以发现已经将所有middleware串联
起来了,并同时修改了dispatch的指向
。最后看一下这时候compose执行返回,如下:
dispatch = a(b(c(dispatch)))
调用dispatch(action),执行循序:
1. 调用 a(b(c(dispatch)))(action) __print__: 第1层中间件2. 返回 a: next(action) 即b(c(dispatch))(action)3. 调用 b(c(dispatch))(action) __print__: 第2层中间件4. 返回 b: next(action) 即c(dispatch)(action)5. 调用 c(dispatch)(action) __print__: 根dispatch6. 返回 c: next(action) 即dispatch(action)7. 调用 dispatch(action)
本博客参考文章:
- Redux的中间件原理分析
- 十分钟理解Redux中间件
- 理解 redux 中间件
- 详解redux中间件
React学习笔记——redux里中间件Middleware的运行机理相关推荐
- react组件卸载调用的方法_好程序员web前端培训分享React学习笔记(三)
好程序员web前端培训分享React学习笔记(三),组件的生命周期 React中组件也有生命周期,也就是说也有很多钩子函数供我们使用, 组件的生命周期,我们会分为四个阶段,初始化.运行中.销毁.错误处 ...
- react render没更新_web前端教程分享React学习笔记(一)
web前端教程分享React学习笔记(一),React的起源和发展:React 起源于 Facebook 的内部项目,因为该公司对市场上所有 JavaScript MVC 框架,都不满意,就决定自己写 ...
- React学习笔记:入门案例
React学习笔记:入门案例 React 起源于 Facebook 内部项目,因为市场上所有 JavaScript MVC 框架都不令人满意,公司就决定自己写一套,用来架设 Instagram 的网站 ...
- React学习笔记(五) 状态提升
状态提升究竟是什么东西呢?别急,下面让我们一步一步来看看究竟要怎么使用状态提升 假设我们有这样一个需求,提供两个输入框(分别属于两个组件),保证输入框里面的内容同步 好,下面我们先来封装一个输入框组件 ...
- react学习笔记1--基础知识
什么是react A JAVASCRIPT LIBRARY FOR BUILDING USER INTERFACES[React是一个用于构建用户界面的JavaScript库.] React之所以快, ...
- React学习笔记 - 组件Props
React Learn Note 4 React学习笔记(四) 标签(空格分隔): React JavaScript 三.组件&Props 组件可以将UI切分成一些独立的.可复用的部件,这样你 ...
- IOS学习笔记05---C语言程序的开发运行过程
IOS学习笔记05---C语言程序的开发运行过程 0 5.C语言3-C语言程序的开发运行过程 ----------------------------------------------------- ...
- 2022Java学习笔记七十三(异常处理:运行时异常、编译时异常、异常的默认处理的流程)
2022Java学习笔记七十三(异常处理:运行时异常.编译时异常.异常的默认处理的流程) 一.异常体系 1.Exception:java.lang包下,称为异常类,它表示程序本身可以处理的问题 2.R ...
- React学习笔记八-受控与非受控组件
此文章是本人在学习React的时候,写下的学习笔记,在此纪录和分享.此为第八篇,主要介绍非受控组件与受控组件. 目录 1.非受控组件 1.1表单提交案例 1.2案例的总结 2.受控组件 2.1受控组件 ...
最新文章
- Android RadioButton 修改选择框
- wxWidgets:使用文本模板
- 原型模式 —— Java的赋值、浅克隆和深度克隆的区别
- 如何基于 Notadd 构建 API (Laravel 写 API)
- mysql按日期获取最新_mysql获取按日期排序获取最新的记录
- 蓝图中实现人物移动1
- WPF界面设计技巧(11)-认知流文档 小议WPF的野心
- Python扑克牌发牌(用类实现)
- 王者荣耀服务器维护中有什么漏洞,王者荣耀:排位惊现漏洞,利用这个BUG一天上王者,三天登荣耀...
- 【MTSP】基于matlab粒子群优化蚁群算法求解多旅行商问题【含Matlab源码 1616期】
- JAVA GUI创作简易记牌器
- Redis-stack 初体验
- 我查查 6.6 去校验分析
- python百度爬虫_Python爬虫 - 简单抓取百度指数
- 服务器被攻击的常见手段以及解决方法
- java 异常之Cause: org.apache.ibatis.executor.ExecutorException: Executor was closed
- Beyond compare使用
- EasyNVR显示级联成功,EasyNVS平台上并无通道是什么原因?
- 个人理解ToB和ToC业务的不同点
- 分享一份完整内容高端企业项目成本管理培训PPT模板
热门文章
- B端和C端产品的理解
- 多模态情感分析研究综述 论文笔记
- 如何删除下一页分节符_页面布局里分节符添加的下一页空白页怎么删 - 卡饭网...
- 学校计算机室应该配备哪种灭火器,学校教学楼应配备哪种灭火器
- 极智AI | 量化实现分享五:详解格灵深瞳 EQ 量化算法实现
- CodeForces - 1255B Fridge Lockers 思维+建图)
- QT运行时报错Gtk-Message: 20:31:49.219: Failed to load module “canberra-gtk-module
- Android All flavors must now belong to a named flavor dimension
- XDU 1028 G.锘爷考驾照
- 外星人显卡拓展坞支持linux,外星人扩展坞可以用哪些显卡?