资讯专栏INFORMATION COLUMN

深度剖析 redux applyMiddleware 中 compose 构建异步数据流的思路

tinylcy / 935人阅读

摘要:前言本文作者站在自己的角度深入浅出算了别这么装逼分析在设计过程中通过构建异步数据流的思路。看上去那是相当的完美,根据咱们写代码的思路咱们来比对一下原版吧。时直接返回传入函数函数。

前言
本文作者站在自己的角度深入浅出...算了别这么装逼分析 redux applyMiddleware 在设计过程中通过 compose 构建异步数据流的思路。自己假设的一些场景帮助理解,希望大家在有异步数据流并且使用redux的过程中能够有自己的思路(脱离thunk or saga)构建自己的 enhancer.如果你看完本文之后还想对我有更多的了解,可以移步我的github;
正文

实际场景中遇到一个这样的问题:商品详情页的微信页面,未注册的用户点击购买一个商品,我们希望能够实现静默登录就有如下几个步骤:

获取code;

获取openId、AccessToken;

根据openId、获取openId、AccessToken;获取用户信息实现自动注册然后登录;

跳到商品购买页。

这是就是一个典型异步数据流的过程。在上一个函数执行到某个时候再去调用下一个函数,使得这些个函数能够顺序执行。我们简化一下,构建如下的函数数组使得他们能够顺序执行吧:

const fucArr = [
    next=>{
        setTimeout(()=>{
              console.log(1);
              next()
        }, 300)
    },
    next=>{
        setTimeout(()=>{
              console.log(2);
              next()
        }, 200)
      },
    next=>{
        setTimeout(()=>{
              console.log(3);
              next()
        }, 100)
    }
]

撸起袖子就开始干了起来,有三个函数,基于走一步看一步思想(瞎胡说的)那我就先执行两个吧

    fucArr[0]( fucArr[1] );// funcArr[1] 运行报错 TypeError: next is not a function

报错,因为fucArr[1]中有next函数调用,也得接收一个函数,这下就麻烦了,fucArr[1]又不能直接传参调用(因为会比fucArr[0]先执行),于是乎我们需要婉转一点。

    fucArr[0]( ()=>fucArr[1](()=>{}) ); //1 2 
两个函数顺序执行搞定了那三个函数岂不是,没错,小case。
    fucArr[0]( ()=>fucArr[1](()=>{ fucArr[2](()=>{}) }) );// 1 2 3

那我想在数组后面再加一个函数内心os:不加,去死,这样写下去真是要没玩没了了;

既然是个数组,那咱们就循环吧,思路肯定是:1.下个函数重新整合一下,作为参数往上一个函数传;2.当到遍历到数组末尾的时候传入一个空函数进去避免报错。

OK开始,既然是循环那就来个for循环吧,既然是下一个函数传给上一个当参数,得让相邻的两个函数出现在同一个循环里啦。于是有了起手式:

    for (let index = 0; index < fucArr.length; index++) {
        const current = array[index];
        const next = array[index + 1];
        current(()=>next())
    }

起手后发现不对呀,我需要喝口热水,压压惊,冷静一下,仔细观察一下上面咱们代码的结构发现咱们的函数结构其实是酱紫的:

    a(()=>{
        b(c)
    })

实际就上上一个函数调用被 ()=> 包裹后的下一个函数直接调用并传入一个函数c,而函数c会在函数b的运行的某个时刻被调用,并且能接收下一个函数作为参数然后......再说下去就没玩没了了,因此c函数的模式其实也是被一个()=>{}包裹住的函数;然后再观察我们上面的模式没有c传递,因此模式应该是:

    a(c=>{
        b(c)
    })
    // 我们再往下写一层
    a(
        d=>{
            (
                c=>b(c)
            )(
                d=>c(d)
            )// 为了避免你们看不懂我在写啥,我告诉你你,这玩意儿是函数自调用
        }
    )
    // 怎么样是不是有一种豁然开朗的赶脚

我们发现每次新加入一个函数,都是重新构建一次a函数里的参数,以下我将这个参数简称函数d

于是乎我们来通过循环构建这个d

为了让循环体都能拿到d,因此它肯定是在循环的上层作用域

而且d具有两个特性:

能接受一个函数作为参数,这个函数还能接收另一个函数作为参数,并会在某个时刻进行调用

每次循环都会根据当前d,然后加入当前函数,按照相同模式进行重构;

ps: 我们发现这两个特性其实和咱们传入的每个函数特性是一致的。

   于是乎咱们把第一个数组的函数组作为起始函数:
    var statusRecord = fucArr[0];
    for (let index = 1; index < fucArr.length; index++) {
        statusRecord = next=>statusRecord(()=>fucArr[index](next))
    }
  写完发现这样是错误的,如果调用函数statusRecord那就会变成,自己调自己,自己调自己,自己调自己,自己调自己~~皮一下很开心~~...的无限递归。
  在循环记录当前状态的场景下,有一个经典的demo大家了解过:在一个li列表中注册点击事件,点击后alert出当前index;具体就不详述了于是statusRecord,就改写成了下面这样
    statusRecord = ((statusRecord)=>(next)=>statusRecord(()=>fucArr[index](next))(statusRecord))
  为什么index不传呢?因为index是let定义,可以看做块级作用域,又有人要说js没有块级作用域,我:你说得对,再见。
  最后咱们得到的还是这个模型要调用,别忘了传入一个函数功最后数组最后一个函数调用。不然会报错
    statusRecord(()=>{}) // 输出1、2、3
那咱们的功能就此实现了;不过可以优化一哈。咱们上面的代码有几个要素:

数组循环

状态传递

初始状态为数组的第一个元素

最终需要拿到单一的返回值

不就是活脱脱用来描述reduce的吗?于是乎我们可以这样撸
    //pre 前一个状态、 cur当前循环函数、next 待接收的下一个
      fucArr.reduce((pre, cur)=>{
          return (next)=>pre(()=>cur(next))
      })(()=>{})// 1 2 3
   以上异步顺序调用的问题咱们已经理解了,咱们依次输出了1,2,3。但是咱们现实业务中常常是下一个函数执行,和上一个函数执行结果是关联的。咱们就想能不能改动题目贴合实际场景,上一个函数告诉下一个函数`console.log(n)`,于是乎题目做了一个小调整。
    const fucArr = [
        next=>{
            setTimeout(()=>{
                console.log(1);
                next(2)
            }, 300)
        },
        // 函数2
        (next,n)=>{
        console.log(n);
            next(3)
        },
        // 函数3
        (next,n)=>{
        console.log(n);
            next(4)
        }
    ]

    fucArr.reduce((pre,cur)=>{
        return (next)=>pre((n)=>cur(next,n))
    })((n)=>{console.log(n)})// 1 2 3 4
   哇,功能又实现了,我们真棒。现在我们来回忆一下redux里中间件里传入函数格式
store=>next=>action=>{
    // dosomething...
    next()
}
    在某一步中store会被剥掉,在这就不细说了,于是咱们题目再变个种
    const fucArr = [
        next=>n=>{
            setTimeout(()=>{
                console.log(n);
                next(n+1)
            }, 300)
        },
        // 函数2
        next=>n=>{
            setTimeout(()=>{
                console.log(n);
                next(n+1)
            }, 300)
        },
        // 函数3
        next=>n=>{
            setTimeout(()=>{
                console.log(n);
                next(n+1)
            }, 300)
        }
    ]

卧槽,我们发现之于之前遇到的问题,这个实现就舒服很多了。因为你传入的函数应该是直接调用,因为我们需要的调用的函数体其实是传入函数调用后返回的那个函数,不需要我们通过()=>{...}这种额外的包装。
于是咱们的实现就变成了:

    fucArr.reduce((pre,cur)=>{
        return (next)=>pre(cur(next))
    })((n)=>{console.log(n)})

我们自信满满的node xxx.js了一下发现?????what fuck 为啥什么都没有输出,喝第二口水压压惊分析一下:

    // before 之前的第一个函数和函数模型
    next=>{
        setTimeout(()=>{
            console.log(1);
            next(n+1)
        }, 300)
    }
    a(c=>{
        b(c)
    })

    // ------------
    // after 现在的第一个函数和函数模型
    next=>n=>{
        setTimeout(()=>{
            console.log(n);
            next(n+1)
        }, 300)
    }
    a(b(c))
    // 发现现在的第一个函数调用之后,一个函数。这个函数还要再接收一个参数去启动

(⊙v⊙)嗯没错,经过精妙的分析我知道要怎么做了。

    fucArr.reduce((pre,cur)=>{
        return (next)=>pre(cur(next))
    })((n)=>{console.log(n)})(1)// 1 2 3 4

我们来把这个功能包装成方法,就叫他compose好了。

    const compose = fucArr=>{
        if(fucArr.length === 0) return;
        if(fucArr.length === 1)    return fucArr[0]((n)=>{console.log(n)})(1)
        fucArr.reduce((pre,cur)=>{
            return (next)=>pre(cur(next))
        })((n)=>{console.log(n)})(1)
    }

看上去那是相当的完美,根据咱们写代码的思路咱们来比对一下原版吧。

length === 0 时: 返回一个传入什么返回什么的函数。

length === 1 时: 直接返回传入函数函数。

length > 1 时: 构建一个a(b(c(....)))这种函数调用模型并返回,使用者自定义最后一环需要运行的函数,并且能够定义进入第一环的初始参数

    // 原版
    function compose(...funcs) {
        if (funcs.length === 0) {
            return arg => arg
        }

        if (funcs.length === 1) {
            return funcs[0]
        }

        return funcs.reduce((a, b) => (...args) => a(b(...args)))
    }
结语
最后说一点题外话,在整个实现的过程中确保异步调用顺序还有很多方式。亲测可用的方式有:

bind

递归调用

通过new Promise 函数,将resolve作为参数方法传入上一个函数然后改变Promise状态...,

如果大家有兴趣可以自己实现一下,为了不把大家的思路带歪,在写的过程中并没有体现出来。

感谢@MrTreasure帮我指出文章中的问题,如果觉得我写对你有一定的帮助,那就点个赞吧,因为您的鼓励是我最大的动力。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/100310.html

相关文章

  • Redux专题:间件

    摘要:好处就是不再需要能够处理异步的中间件了。不过,它是一个研究中间件很好的范本。执行它,返回的是由第二层函数组成的中间件数组。也就是说呀同学们,除了最后一个中间件的是原始的之外,倒数往前的中间件传入的都是上一个中间件的逻辑函数。 本文是『horseshoe·Redux专题』系列文章之一,后续会有更多专题推出来我的 GitHub repo 阅读完整的专题文章来我的 个人博客 获得无与伦比的阅...

    ybak 评论0 收藏0
  • redux源码解读--applyMiddleware源码解析

    摘要:的中间件主要是通过模块实现的。返回的也是一个对象这个其实就是,各个中间件的最底层第三层的哪个函数组成的圆环函数构成的这就是对源码的一个整体解读,水平有限,欢迎拍砖。后续的源码解读和测试例子可以关注源码解读仓库 applyMiddleware源码解析 中间件机制在redux中是强大且便捷的,利用redux的中间件我们能够实现日志记录,异步调用等多种十分实用的功能。redux的中间件主要是...

    Atom 评论0 收藏0
  • redux middleware 详解

    摘要:执行完后,获得数组,,它保存的对象是图中绿色箭头指向的匿名函数,因为闭包,每个匿名函数都可以访问相同的,即。是函数式编程中的组合,将中的所有匿名函数,,组装成一个新的函数,即新的,当新执行时,,从左到右依次执行所以顺序很重要。 前言 It provides a third-party extension point between dispatching anaction, and t...

    yanwei 评论0 收藏0
  • 走近 Redux

    摘要:的核心思想就是维护一个单向数据流,数据的流向永远是单向的,所以每个步骤便是可预测的,程序的健壮性得到了保证。另外,还有一点比较重要的是,因为没有了一个一直保存更新的状态对象,所以在中的也就没有意义了,通过可以完全实现一个顺畅的数据流。 1 Redux Redux is a predictable state container for JavaScript apps 简单来说,Redu...

    fevin 评论0 收藏0
  • Redux原理分析

    摘要:调用链中最后一个会接受真实的的方法作为参数,并借此结束调用链。总结我们常用的一般是除了和之外的方法,那个理解明白了,对于以后出现的问题会有很大帮助,本文只是针对最基础的进行解析,之后有机会继续解析对他的封装 前言 虽然一直使用redux+react-redux,但是并没有真正去讲redux最基础的部分理解透彻,我觉得理解明白redux会对react-redux有一个透彻的理解。 其实,...

    sumory 评论0 收藏0

发表评论

0条评论

tinylcy

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<