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.mzph.cn/diannao/52937.shtml

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

相关文章

音视频开发之旅(90)-Vision Transformer论文解读与源码分析

目录 1.背景和问题 2.Vision Transformer(VIT)模型结构 3.Patch Embedding 4.实现效果 5.代码解析 6.资料 一、背景和问题 上一篇我们学习了Transformer的原理,主要介绍了在NLP领域上的应用,那么在CV(图像视频)领域该如何使用? 最直观…

C# 获取当前鼠标位置

在C#中,获取当前鼠标位置可以通过多种方式实现,但最常见和直接的方法之一是使用System.Windows.Forms命名空间中的Cursor类或者Control类的PointToClient(如果你正在处理WinForms应用程序)或Windows.UI.Core.CoreWindow的PointerP…

Java源码学习之高并发编程基础——AQS源码剖析之阻塞队列(下)

1.前言&目录 前言: 在上一篇文章AQS源码剖析之阻塞队列(上)中介绍了以独占锁模式下AQS的基本原理,AQS仅仅起到了一个“维持线程等待秩序”的作用,那么本篇文章继续讲解共享锁模式下的特点。 AQS不操纵锁的获取或者…

算法复盘——LeetCode hot100:哈希

文章目录 哈希表哈希表的基本概念哈希表的使用1. 插入操作2. 查找操作3. 删除操作 哈希表的优点和缺点1.两数之和复盘 242.有效的字母异位词复盘 49.字母异位词分组复盘 128. 最长连续序列复盘HashSet 哈希表 先来搞清楚什么是哈希表吧~ 概念不清楚方法不清楚怎么做题捏 哈希表…

问:说一下Java中数组的实例化方式有哪些?

在Java中,数组的实例化可以通过多种方式完成。以下是五种不同的实例化数组的方式。 1. 直接初始化 这种方式在声明数组的同时,直接初始化数组的元素。 // 示例:直接初始化一个整型数组 int[] numbers {1, 2, 3, 4, 5}; // 解释&#xff1…

使用mysql保存密码

登录MySQL 这行命令告诉MySQL客户端程序用户root准备登录,-p表示告诉 MySQL 客户端程序提示输入密码。 mysql -u root -p创建数据库 create database wifi; use wifi;create table password(user_password CHAR(8),primary key(user_password));源码 代码编译 …

C#——类与结构

在未学习面向对象语言时,我常常将类比作一种结构体,其实类与结构体也确实很相似,类用来做函数的集合,结构用来做变量的集合,接下来将从几个角度刨析类与结构的不同。 类 vs 结构 类和结构在设计和使用时有不同的考虑…

学习记录:js算法(二十):子数组最大平均数 I、无重复字符的最长子串

文章目录 子数组最大平均数 I我的思路网上思路 无重复字符的最长子串我的思路网上思路 总结 子数组最大平均数 I 给你一个由 n 个元素组成的整数数组 nums 和一个整数 k 。 请你找出平均数最大且 长度为 k 的连续子数组,并输出该最大平均数。 任何误差小于 10-5 的答…

QT实战项目之音乐播放器

项目效果演示 myMusicShow 项目概述 在本QT音乐播放器实战项目中,开发环境使用的是QT Creator5.14版本。该项目实现了音乐播放器的基本功能,例如开始播放、停止播放、下一首播放、上一首播放、调节音量、调节倍速、设置音乐播放模式等。同时还具备搜索功…

Centos 下载和 VM 虚拟机安装

1. Centos 下载 阿里云下载地址 centos-7.9.2009-isos-x86_64安装包下载_开源镜像站-阿里云 2. VM 中创建 Centos 虚拟机 2.1 先打开 VM 虚拟机,点击首页的创建新的虚拟机 2.2 选择自定义,然后点击下一步。 2.3 这里默认就好,继续选择下一…

docker加速配置 daemon.json

配置docker 加速 提高下载速度 在某些地区,尤其是中国,Docker Hub 的访问速度可能较慢,导致镜像下载时间过长。通过配置加速器,可以显著提高镜像的拉取速度,减少等待时间。 减少网络不稳定带来的影响 网络不稳定可能导…

gitlab SSH的使用

一、 安装git bash https://git-scm.com/download/win 下载windows 版本,默认安装即可。 二、使用命令 打开本地git bash,使用如下命令生成ssh公钥和私钥对 ssh-keygen -t rsa -C ‘xxxxxx.com’ 然后一路回车 (-C 参数是你的邮箱地址) 若是想输入密码可以输入…

算法-最长连续序列

leetcode的题目链接 这道题的思路主要是要求在O(n)的时间复杂度下,所以你暴力解决肯定不行,暴力至少两层for循环,所以要在O(n)的时间复杂度下,你可以使用HashSet来存储数组,对于每个数字&#…

黑马JavaWeb开发笔记07——Ajax、Axios请求、前后端分离开发介绍、Yapi详细配置步骤

文章目录 前言一、Ajax1. 概述2. 作用3. 同步异步4. 原生Ajax请求(了解即可)5. Axios(重点)5.1 基本使用5.2 Axios别名(简化书写) 二、前后端分离开发1. 介绍1.1 前后台混合开发1.2 前后台分离开发方式&…

Docker续6:容器网络

1.bridge-utils 一个用于Linux系统的网络桥接工具集。它提供了一些命令行工具,帮助用户创建、管理和配置网络桥接。网络桥接是一种将多个网络接口连接在一起,以使它们能够作为单个网络段进行通信的技术。 bridge-utils 常用的命令包括: b…

【 OpenHarmony 系统应用源码魔改 】-- Launcher 之「桌面布局定制」

前言 阅读本篇文章之前,有几个需要说明一下: 调试设备:平板,如果你是开发者手机,一样可以加 Log 调试,源码仍然是手机和平板一起分析;文章中的 Log 信息所显示的数值可能跟你的设备不一样&…

单片机编程魔法师-并行多任务程序

程序架构 程序代码 小结 数码分离,本质上就是将数据和代码逻辑进行分离,跟第一章使用数据驱动程序一样的道理。 不过这里不同之处在于。这里使用通过任务线程,但是却有2个任务在运行,两个任务都通过先初始化任务数据参数&#x…

this.$nextTick() 是 Vue.js 提供的一个方法

this.$nextTick() 是 Vue.js 提供的一个方法,用于在 DOM 更新完成后执行指定的代码。它的作用主要是确保在 Vue.js 完成 DOM 更新后,再执行某些依赖于更新的操作。这个方法通常用于处理需要在视图更新后立即进行的操作,如获取最新的 DOM 元素…

SQLite的安装和使用

一、官网链接下载安装包 点击跳转 步骤:点击安装这个红框的dll以及红框下面的tools (如果有navicat可以免上面这个安装步骤,安装上面这个是为了能在命令行敲SQL而已) 二、SQLite的特点 嵌入的(无服务器的&#x…

使用MCP2518FD在STM32G4上实现SPI转CAN通信

在汽车电子和工业控制系统中,CAN(Controller Area Network)总线是一种广泛使用的通信协议。MCP2518FD是一款由Microchip生产的CAN控制器,它支持SPI通信接口,非常适合与STM32等微控制器配合使用。本文将介绍如何在STM32…