Redux的中间件原理分析

Redux的中间件原理分析

redux的中间件对于使用过redux的各位都不会感到陌生,通过应用上我们需要的所有要应用在redux流程上的中间件,我们可以加强dispatch的功能。最近抽了点时间把之前整理分析过的中间件有关的东西放在这里分享分享。本文只对中间件涉及到的createStore、applyMiddleware以及典型常用中间的的源码做解析,让大家了解redux的内部模块:createStore.js、applyMiddleware.js,以及redux的中间件之间是怎么串联在一起并协作工作的。文章内容特别是源码部分对函数式编程思想有一定要求,比如:柯里化、compose等,源码中会大量涉及到这些概念,如果读者对此是不熟悉,可先学习这方面相关资料。

一、thunk作为一个典型redux中间件,它做了什么事?

简单的thunk使用方式如下:

// action
const getUserInfo = (id) => {return function (dispatch, getState, extraArgument){return reqGet({id: id}).then(res => res.json().data).then(info => {dispatch({type: "GET_USER_INFO",info})}).catch(err => console.log('reqGet error: ' + err));}};
// dispatch action
dispatch(getUserInfo(1));

在上述使用实例中,我们应用thunk中间到redux后,可以dispatch一个方法,在方法内部我们想要真正dispatch一个action对象的时候再执行dispatch即可,特别是异步操作时非常方便。当然支持异步操作的redux中间件也并非只有thunik,还有更专业的其他中间件,这非本文内容,这里不再多讲。

二、thunk中间件内部是什么样的?

thunk源码如下(为了方便阅读,源码中的箭头函数在这里换成了普通函数):

function createThunkMiddleware (extraArgument){return function ({dispatch, getState}){return function (next){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是一个很常用的redux中间件,应用它之后,我们可以dispatch一个方法,而不仅限于一个纯的action对象。它的源码也很简单,如上所示,除去语法固定格式也就区区几行。

下面我们就来看看源码(为了方便阅读,源码中的箭头函数在这里换成了普通函数),首先是这三层柯里化:

// 外层function createThunkMiddleware (extraArgument){// 第一层return function ({dispatch, getState}){// 第二层return function (next){// 第三层return function (action){if (typeof action === 'function'){return action(dispatch, getState, extraArgument);}return next(action);};}}
}

首先是外层,从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不是一个函数,就走之前解析第二层时提到的步骤。

三层的初步解析就到这里,通过这个分析,其实也没有得出很重要的结论,对于想要了解applyMiddleware到底干了啥,我们还是很懵逼的。但至少我们可以初步判断出第一层到第三层均为applyMiddleware对一个redux中间件的基本写法要求,也就是说无论一个中间件要实现一个怎样的功能,其固定格式必须是这个,在第三层函数内部才是自己功能逻辑实现的地方。

记住这三层做的事情很重要(虽然凭借着这极少的信息,我们依然很懵逼),但在下一个段落中,我们将再次提到它们,并详细说明为什么会有这三层柯里化的存在。

三、applyMiddleware内部是怎样的?createStore又干了什么?

直接上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)};const chain = middlewares.map(middleware => middleware(middlewareAPI));dispatch = compose(...chain)(store.dispatch);return {...store,dispatch};}}
}

从其源码可以看出,applyMiddleware内部一开始也是两层柯里化,我们从thunk过来本来是为了寻找答案的,这让我们一过来就又处于懵逼之中,为啥这么多柯里化?哈哈,解铃还须系铃人,让我们先来看看和applyMiddleware最有关系的createStore的主要源码:

export default function createStore(reducer, preloadedState, enhancer) {if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {enhancer = preloadedStatepreloadedState = undefined}if (typeof enhancer !== 'undefined') {if (typeof enhancer !== 'function') {throw new Error('Expected the enhancer to be a function.')}return enhancer(createStore)(reducer, preloadedState)}if (typeof reducer !== 'function') {throw new Error('Expected the reducer to be a function.')}var currentReducer = reducer;var currentState = preloadedState;var currentListeners = [];var nextListeners = currentListeners;var isDispatching = false;function ensureCanMutateNextListeners (){// ...    }function dispatch (){// ...    }function subscribe (){// ...    }function getState (){// ...    }function replaceReducer (){// ...    }function observable (){// ...    }dispatch({ type: ActionTypes.INIT })return {dispatch,subscribe,getState,replaceReducer,[$$observable]: observable}
}

对于createStore的源码我们只需要关注和applyMiddleware有关的地方,其他和store有关的不是本文的重点。从其内部前面一部分代码来看,其实很简单,就是对调用createStore时传入的

参数进行一个判断,并对参数做矫正,再决定以哪种方式来执行后续代码。据此可以得出createStore有多种使用方法,根据第一段参数判断规则,我们可以得出createStore的两种使用方式,它们和第一章节中的使用方式相同:

const store = createStore(reducer, {a: 1, b: 2}, applyMiddleware(...));

以及:

const store = createStore(reducer, applyMiddleware(...));

同时根据第一段参数判断规则,我们还可以肯定的是:applyMiddleware返回的一定是一个函数,在上述章节中我们曾猜想过,经过了各个中间件处理以后,原始的store.dispatch会被改造,但最终还是会返回一个经过改造后的dispatch,这里可以确定至少一半是正确了的。

经过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等方法的对象。

四、回过头对applyMiddleware做深入分析

applyMiddleware源码和中间件thunk的源码在第三章节和第一章节中有提到,这里就不再贴出来了,回看前面章节中的源码即可。对于applyMiddleware开头的两层柯里化的出现原因以及和createStore有关的方面,在上述章节章节中已有分析。这里主要针对本文的重点,也就是中间件是如何通过applyMiddleware的工作起来并实现挨个串联的原因做分析。

在第二章节中,我们提到过怀疑在thunk的第一层柯里化中传入的对象是一个类似于store的对象,通过上个章节中applyMiddleware的确实可以确认了,确实如我们所想一样。

接下来这几段代码是整个applyMiddleware的核心部分,也解释了在第二章节中,我们对thunk中间件为啥有三层柯里化的疑虑,把这些代码单独贴出来,如下:

// ...
const chain = middlewares.map(middleware => middleware(middlewareAPI));dispatch = compose(...chain)(store.dispatch);
return {...store,dispatch
};
// ...

首先,applyMiddleware的执行结果最终是返回store的所有方法和一个dispatch方法。这个dispatch方法是怎么来的呢?我们来看头两行代码,这两行代码也是所有中间件被串联起来的核心部分实现,它们也决定了中间件内部为啥会有我们在之前章节中提到的三层柯里化的固定格式,先看第一行代码:

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

遍历所有的中间件,并调用它们,传入那个类似于store的对象middlewareAPI,这会导致中间件中第一层柯里化函数被调用,并返回一个接收next(即dispatch)方法作为参数的新函数。为什么会有这一层柯里化呢,主要原因还是考虑到中间件内部会有调用store方法的需求,所以我们需要在此注入相关的方法,其内存函数可以通过闭包的方式来获取并调用,若有需要的话。

遍历结束以后,我们拿到了一个包含所有中间件新返回的函数的一个数组,将其赋值给变量chain,译为函数链。

再来看第二句代码:

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

我们展开了这个数组,并将其内部的元素(函数)传给了compose函数,compose函数又返回了我们一个新函数。然后我们再调用这个新函数并传入了原始的未经任何修改的dispatch方法,

最后返回一个经过了修改的新的dispatch方法。

有几点疑惑:

\1. 什么是compose?在函数式编程中,compose指接收多个函数作为参数,并返回一个新的函数的方式。调用新函数后传入一个初始的值作为参数,该参数经最后一个函数调用,将结果返回并作为倒数第二个函数的入参,倒数第二个函数调用完后,将其结果返回并作为倒数第三个函数的入参,依次调用,知道最后调用完传入compose的所有的函数后,返回一个最后的结果。这个结果就是把初始的值经过传入compose中的个函数改造后的结果,一个简易的compose实现如下:

function compose (...fncs){fncs = fncs.reverse();let result;return function (arg){result = arg;for (let fnc of fncs){result = fnc(result);}return result;}
}

compose是从右到昨依次调用传入其内部的函数链,还有一种从左到右的方式叫做pipe,即去掉compose源码中的对函数链数组的reverse即可。

从上面对compose的分析中,不难看出,它就实现了对我们中间件的串联,并对原始的dispatch方法的改造。

在第二章节中,thunk中间件的第二层柯里化函数即在compose内部被调用,并接收了经其右边那个中间函数改造并返回dispatch方法作为入参,并返回一个新的函数,再在该函数内部添加自己的逻辑,最后调用右边那个中间函数改造并返回dispatch方法接着执行前一个中间件的逻辑。当然如果只有一个thunk中间件被应用了,或者他出入传入compose时的最后一个中间件,那么传入的dispatch方法即为原始的store.dispatch方法。

thunk的第三层柯里化函数,即为被thunk改造后的dispatch方法:

// ...
return function (action){// thunk的内部逻辑if (typeof action === 'function'){return action(dispatch, getState, extraArgument);}// 调用经下一个中间件(在compose中为之前的中间件)改造后的dispatch方法(本层洋葱壳的下一层),并传入actionreturn next(action);
};

这个改造后的dispatch函数将通过compose传入thunk左边的那个中间件作为入参。

经上述分析,我们可以得出一个中间件的串联和执行时的流程,以下面这段使用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的执行顺序是从左至右的,因为最后被包裹的中间件,将被最先执行。

如图所示:

当我们剥壳的时候,剥一层壳,执行一层的逻辑,即走一层中间件的功能,直至调用藏在最里边的原始的store.dispatch方法去派发action。这样一来我们就不需要在每次派发action的时候再写单独的代码逻辑的。

总结来说就是:

在中间件串联的时候,middleware1-3的串联顺序是从右至左的,也就是middleware3被包裹在了最里面,它内部含有对原始的store.dispatch的调用,middleware1被包裹在了最外边。

当我们在业务代码中dispatch一个action时,也就是中间件执行的时候,middleware1-3的执行顺序是从左至右的,因为最后被包裹的中间件,将被最先执行。

如图所示:
在这里插入图片描述

至此为止,关于applyMiddleware和thunk中间件的分析就完成了,如果问题和不清楚之处烦请指出。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/413770.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Leetcode 404-左叶子之和

题目 给定二叉树的根节点 root ,返回所有左叶子之和。 题解 二叉树的题目,如果需要返回某个值,可以分左右子树递归计算,最后sumleftright 递归三部曲: 确定递归函数的参数和返回值 判断一个树的左叶子节点之和&…

插入排序

插入排序是一种简单直观的排序算法。它的基本思想是将待排序的数据分成已排序和未排序两部分,每次从未排序部分选择一个元素插入到已排序部分的合适位置,直到未排序部分为空。 插入排序是一种简单直观的排序算法,它的基本思想是将一个元素插…

Windows系统安装MySQL

下载MySQL 打开网址MySQL :: Download MySQL Community Server点击图下所示位置Download 进入图下所示界面,点击图下所示位置不登录下载 已下载完成 安装MySQL 将下载好的压缩包解压到一个专门的位置,该软件为绿色版软件,解压即可使用 配置…

Open3D mesh Taubin滤波

目录 一、概述 1.1原理 1.2实现步骤 1.3应用场景 二、代码实现 2.1关键函数 参数详解 返回值 2.2完整代码 三、实现效果 3.1加入噪声的mesh 3.2Taubin迭代10次 3.3Taubin迭代100次 Open3D点云算法汇总及实战案例汇总的目录地址: Open3D点云算法与点云…

分布式云扩展 AI 边缘算力,助力用户智能化创新

近期,AI 创新圈再次发布重磅产品更新。OpenAI 全新旗舰版多模态模型 GPT-4o 横空出世,其打通文本、图像、视频的富媒体理解能力以及敏捷的智能化对话,将 AI 助手的人性化表达效果,提升至更高水平。 ​ 从技术源头来看&#xff0c…

数据线性结构

一、线性表 优点:可以很快速的找到内存地址 查询,修改快 缺点:在中间部分新增,删除部时需要移动后续的元素 像java中的stream流的过滤等操作都是新建立一个集合有序插入返回,空间换时间 java中list下标为什么要从0开…

网工面试题(安全)

上一篇:网工面试题(数通) 防火墙 防火墙的应用场景 防火墙:部署在网络出口处/服务器区(数据中心)/广域网接入,用于防止外界黑客攻击、保护内网安全硬件。 传统防火墙和下一代防护墙的区别 传统防火墙的功能…

AJAX day-02 HTTP格式JSON格式

目录 一. 计算机网络 1.1 网络参考模型 1.2 各层重要对应的协议 1.3 DNS解析(域名解析服务器) 1.4 FTP(文件传输协议) 1.5 UDP(用户数据报协议) 1.6 TCP(传输控制协议) 1.7 IP(网际互连协议) 1.8 …

golang本地缓存fastcache高性能实现原理

1. git仓库 https://github.com/abbothzhang/fastcache 2. 整体原理 initCache时不会申请内存,只有第一次set时候才会申请,且会一次性申请64MB,后面不够了又一次性申请1024*64MB大小内存 2.1. 时序图 3. 高性能原因 将cache分为512个buc…

C++奇迹之旅:深度解析list的模拟实现

文章目录 📝前言🌠list节点🌉list 🌠迭代器的创建🌉const迭代器 🌠代码🚩总结 📝前言 🌠list节点 我们先建立一个列表里的节点类listnode,用来构造list的节…

数据仓库系列 3:数据仓库的主要组成部分有哪些?

你是否曾经好奇过,当你在网上购物或使用手机应用时,背后的数据是如何被存储和分析的?答案就在数据仓库中。本文将为你揭开数据仓库的神秘面纱,深入探讨其核心组成部分,以及这些组件如何协同工作,将海量数据转化为有价值的商业洞察。 目录 引言:数据仓库的魔力1. 数据源和数据…

[Algorithm][综合训练][体育课测验(二)][合唱队形][宵暗的妖怪]详细讲解

目录 1.体育课测验(二)1.题目链接2.算法原理详解 && 代码实现 2.合唱队形1.题目链接2.算法原理详解 && 代码实现 3.宵暗的妖怪1.题目链接2.算法原理详解 && 代码实现 1.体育课测验(二) 1.题目链接 体育课测验(二) 2.算法原理详解 && 代码实现…

解决Selenium已安装,在pycharm导入时报错

搭建设selenium环境时,selenium已安装,但是在pycharm中使用“from selenium import webdriver”语句时红线报错 解决方案: 1.file->settings进入设置 2.点击加号,搜索‘selenium’安装 3,等待安装完成&#xff0…

项目技巧二

目录 java中Date和mysql数据库datetime数据类型 注意: 在yml文件中配置成员变量的值 1.写一个yml文件 2.写一个与yml相互映射的类来读取yml的属性信息 3.在其他子模块的配置类中开启此类,读取yml文件的内容信息 4.直接依赖注入(因为已…

Java多进程调用dll程序和exe程序

🎯导读:本文介绍了使用Java调用本地DLL及EXE程序的方法。针对DLL调用,文章提供了基于Java Native Access (JNA) 库的具体实现方案,包括定义Java接口以映射DLL中的函数,并展示了如何加载DLL及调用其中的方法。对于EXE程…

opencv之几何变换

文章目录 1 前言2 线性几何变换的主要类型2.1 平移 (Translation):2.1.1 定义2.1.2代码 2.2 缩放 (Scaling):2.2.1 定义2.2.2 代码 2.3 旋转 (Rotation):2.3.1 定义2.3.2 代码 2.4 仿射变换 (Affine Transformation):2.4.1 定义2.…

数据源10min自动断开连接导致查询抛异常(未获取可用连接)

由于个人能力有限,本文章仅仅代表本人想法,若有不对请即时指出,若有侵权,请联系本人。 1 背景 工作中引入druid来管理数据源连接,由于数据源每隔10分钟强制管理空闲超过10分钟的连接,导致每隔10分钟出现1…

3D打印透气钢与传统透气钢的差异

透气钢作为一种集金属强度与透气性能于一体的特殊材料,在注塑模具领域扮演着关键角色,通过有效排除模具内困气,显著提升制品成型质量与生产效率。当前,市场上主流的透气钢产品多源自日本、美国,其高昂成本与技术壁垒限…

Golang | Leetcode Golang题解之第388题文件的最长绝对路径

题目&#xff1a; 题解&#xff1a; func lengthLongestPath(input string) (ans int) {n : len(input)level : make([]int, n1)for i : 0; i < n; {// 检测当前文件的深度depth : 1for ; i < n && input[i] \t; i {depth}// 统计当前文件名的长度length, isFi…

生成艺术,作品鉴赏:物似主人形

2001年&#xff0c;当21岁的我&#xff0c;还在恒基伟业当高级工程师时。我有一个女同事&#xff0c;她有个特别大的杯子用来喝水&#xff0c;不夸张的说&#xff0c;是那种我从来没见过的大杯子&#xff0c;由于她是很大只的那种&#xff0c;她便自嘲说&#xff1a;「物似主人…