Redux的中间件原理分析

news2024/9/23 19:26:43

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 = preloadedState
        preloadedState = 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方法(本层洋葱壳的下一层),并传入action
    return 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.coloradmin.cn/o/2081389.html

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

相关文章

在线客服系统源码 完全开源可二开 带完整的安装代码包以及搭建部署教程

系统概述 在线客服系统是一种基于互联网技术的客户服务解决方案,通过即时通讯工具,如文本聊天、语音通话、视频交流等方式,实现企业与客户之间的实时互动。它打破了传统客服模式的时空限制,使客户能够在任何时间、任何地点获得快…

Java基础(4)- IDEA

目录 一、Module 1.创建module 2.关闭modue 3.导入module 4.src灰色 二、Package 1.创建package 2.删除package 3.package取名规范 三、类 1.创建类 2.快捷语法 3.HelloWorld 四、IDEA基本设置说明 1.字体 2.提示的快捷键 五、常用快捷键 一、Module 1.创建mod…

Python大数据之Hadoop学习——day05_hive基本操作

一.SQL,Hive和MapReduce的关系 用户在hive上编写sql语句,hive把sql语句转为mapreduce程序去执行 二.Hive的架构映射流程 三.MetaStore元数据管理三种模式 metastore服务配置有3种: 内嵌模式、本地模式、远程模式(推荐) 内嵌模式…

【一文读懂】基于Havenask向量检索+大模型,构建可靠的智能问答服务

Havenask是阿里巴巴智能引擎事业部自研的开源高性能搜索引擎,深度支持了包括淘宝、天猫、菜鸟、高德、饿了么在内的几乎整个阿里的搜索业务。本文针对性介绍了Havenask作为一款高性能的召回搜索引擎,应用在向量检索和LLM智能问答场景的解决方案和核心优势…

泰山派小手机---ubuntu 环境的搭建

问题: 本来我的 泰山派的 ubuntu 虚拟机环境已经搭建好了,但是由于一直到捣鼓 neovim ,把虚拟机 内核搞崩溃,所以从新安装一下 虚拟机。 过程: 1 首先是 安装虚拟机。 下载镜像。 https://mirrors.ustc.edu.cn/ubu…

密码强度验证——js基础积累

//密码强度等级 getPwdLevel:function (pwd,minLength8) {var level 0;if (pwd.length < minLength) return level;if (/\d/.test(pwd)) level; //数字if (/[a-z]/.test(pwd)) level; //小写if (/[A-Z]/.test(pwd)) level; //大写if (/\W/.test(pwd)) level; //特殊字符ret…

安卓好软-----手机端提取apk的小工具 方便简单 无需root权限

apk提取工具 工具小巧。可以提取手机上面当前安装的apk和系统应用apk。而且无需root权限即可正常使用。 效果非常不错。比其他工具提取系统app方便好使。 下载&#xff1a;https://download.csdn.net/download/mg668/89683199?spm1001.2014.3001.5503

副业赚钱新玩法:大模型也能月入过万?

在这个多元化的时代&#xff0c;副业已经成为了越来越多人的选择。无论是为了实现个人兴趣&#xff0c;还是为了增加收入来源&#xff0c;副业都为我们提供了无限可能。而随着人工智能技术的飞速发展&#xff0c;利用大模型来开展副业也成为了一种新兴的方式。今天&#xff0c;…

日常避坑指南:如何正确使用 aiohttp 上传文件,避免文件被提前关闭

在日常开发中,我们经常会遇到需要上传文件到服务器的场景。如果你选择使用 aiohttp 进行异步请求,上传文件的方式需要特别注意,否则可能会遇到一些令人头疼的问题——比如文件被提前关闭,导致上传失败。这篇文章将为你详细解析这个问题,并提供有效的解决方案,帮助你在开发…

Mysql基础练习题 181.找到收入比经理高的员工 (力扣)

181.找到收入比经理高的员工 建表插入数据&#xff1a; Create table If Not Exists Employee (id int, name varchar(255), salary int, managerId varchar(10)); Truncate table Employee insert into Employee (id, name, salary, managerId) values (1, Joe, 70000, 3); …

/lib64/libm.so.6: version `GLIBC_2.27‘ not found 如何解决?

安装GLIBC_2.27 wget http://ftp.gnu.org/gnu/glibc/glibc-2.27.tar.gz tar xf glibc-2.27.tar.gz cd glibc-2.27/ && mkdir build && cd build ../configure --prefix/usr --disable-profile --enable-add-ons --with-headers/usr/include --with-binutils…

聚观早报 | 智界R7亮相;问界新M7 Pro正式上市

聚观早报每日整理最值得关注的行业重点事件&#xff0c;帮助大家及时了解最新行业动态&#xff0c;每日读报&#xff0c;就读聚观365资讯简报。 整理丨Cutie 8月28日消息 智界R7亮相 问界新M7 Pro正式上市 《全职高手3》动画热播 苹果2024秋季发布会官宣 一加Ace 5 Pro设…

火语言RPA流程组件介绍--播放声音

&#x1f6a9;【组件功能】&#xff1a;播放系统声音或指定的Wav格式声音文件 配置预览 配置说明 来源 系统&#xff1a;播放Windows系统内置声音。 自定义Wav文件&#xff1a;播放本地路径下指定声音文件。 Wav文件 支持T或# 默认FLOW输入项 选择Wav声音文件的本地路径。…

35岁零基础转战AI领域:实现AI大模型开发者职业转型的可能性与路径

以下从3个方面帮大家分析&#xff1a; 35岁转行会不会太晚&#xff1f;零基础学习AI大模型开发能不能学会&#xff1f;AI大模型开发行业前景如何&#xff0c;学完后能不能找到好工作&#xff1f; 一、35岁转行会不会太晚&#xff1f; 35岁正处于人生的黄金时期&#xff0c;拥…

灵魂 20 问帮你彻底搞定Transformer

1.Transformer为何使用多头注意力机制&#xff1f;&#xff08;为什么不使用一个头&#xff09; 捕捉多种依赖关系&#xff1a; 多头注意力机制允许模型同时关注输入数据的不同部分和特征。每个“头”都能够学习输入序列的不同表示子空间&#xff0c;从而捕捉到不同类型的依赖关…

大模型落地难点之结构化输出

应用至上 2023年的世界人工智能大会&#xff08;WAIC&#xff09;是“百模大战”&#xff0c;今年WAIC的关键词是“应用至上”。纵观今年论坛热点话题&#xff0c;无论是具身智能还是AI Agent&#xff08;智能体&#xff09;&#xff0c;都指向以大模型为代表的AI技术在不同场…

一文道尽 RAG,为大模型提供你的私有知识

什么是 RAG&#xff1f;先说一个你可能不相信的事实&#xff1a;RAG 是2005年提出的古老技术&#xff08;论文在此 https://arxiv.org/pdf/2005.11401&#xff09;。然后我们先看一个学术定义&#xff1a;“检索增强生成&#xff08;Retrieval-augmented Generation&#xff09…

【MATLAB源码-第198期】基于simulink的三相光伏并网仿真模拟。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 三相光伏并网系统是一种将太阳能转换为电能并将其馈入电网的系统。这个系统通常包括光伏阵列、逆变器&#xff08;包括其控制算法&#xff09;、滤波器、电网连接和监控系统。从上载的框图中可以看出&#xff0c;该系统的设计…

最佳外推发帖器推荐

最佳外推发帖器推荐 外推发帖神器&#xff0c;节省90%发帖时间&#xff01;#搜索留痕运营#百度留痕工具#灰色词排名代发#外推软件推广#B站专栏文章代发 推荐阅读&#xff1a; 哔哩哔哩b站文章专栏发布软件用真实信息注册域名的利弊https://www.bsw80.com/post/2314.html 今…

算法-有效的字母异位词

这道题很简单&#xff0c;就不做过多的解释&#xff0c;只需要创建一个哈希表统计s中出现的次数&#xff0c;然后遍历t&#xff0c;如果没找到&#xff0c;或者找到了但是次数为0则返回错误&#xff0c;否则返回true。代码如下&#xff1a; class Solution { public:bool isAn…