zoukankan      html  css  js  c++  java
  • Redux学习之解读applyMiddleware源码深入middleware工作机制

    随笔前言

    上一周的学习中,我们熟悉了如何通过redux去管理数据,而在这一节中,我们将一起深入到redux的知识中学习。

    首先谈一谈为什么要用到middleware

    我们知道在一个简单的数据流场景中,点击一个button后,在回调中分发一个action,reducer收到action后就会更新state并通知view重新渲染,如下图所示

    但是如果需要打印每一个action来调试,就得去改dispatch或者reducer实现,使其具备打印功能,那么该如何做?因此,需要中间件的加入。

    上图展示了应用middleware后的Redux处理事件的逻辑,每个middleware都可以处理一个相对独立的事物,通过串联不同的middleware实现变化多样的功能!

    小结:Redux中的reducer更加的专注于转化逻辑,所以middleware是为了增强dispatch而出现的。

    middleware是如何工作的

    Redux提供了一个applyMiddleware方法来加载middleware,它的源码是这样的:

    import compose from './compose';
    
    export default function applyMiddleware(...middlewares) {
        return (next) => (reducer, initalState) => {
            let store = next(reducer, initalState);
            let dispatch = store.dispatch;
            let chain = [];
    
            var middlewareAPI = {
                getState: store.getState,
                dispatch: (action) => dispatch(action)
            };
            chain = middlewares.map( middleware => middleware(middlewareAPI));
            dispatch = compose(...chain)(store.dispatch);
    
            return {
                ...store,
                dispatch
            };
    
        }
    
    }
    

    然后我们再上一个logger middleware的源码实现:

    export default store => next => action => {
        console.log('dispatch:', action);
        next(action);
        console.log('finish:', action);
    }
    
    

    虽然看到“源码”的那两个字的时候,内心一万只草什么马奔过,但是一看到代码这么精简,这么优美,那就初读一下源码把。
    然后

    接下来就开始解读上面源码

    深入解析middleware运行原理

    1. 函数式编程思想设计

    middleware是一个层层包裹的匿名函数,这其实是函数式编程的currying(Currying就是把一个带有多个参数的函数拆分成一系列带部分参数的函数)。那么applyMiddleware会对logger这个middleware进行层层的调用,动态的将store和next参数赋值。

    那么currying的middleware结构有什么好处呢?

    • 1.1 易串联: currying函数具有延迟执行的特性,通过不断currying形成的middleware可以积累参数,再配合组合(compose)的方式,这样很容易就形成pipeline来处理数据流
    • 1.2 共享store:在applyMiddleware执行的过程当中,store还是旧的,但是因为闭包的存在,applyMiddleware完成之后,所有的middleware内部拿到的store是最新的且是相同的。

    并且applyMiddleware的结构也是一个多层currying的函数,借助compose,applyMiddleware可以用来和其他插件加强createStore函数

    2. 给middleware分发store

    通过如下方式创建一个普通的store

        let newStore = applyMiddleware(mid1, mid2, mid3, ...)(createStore)(reducer, null);
    

    上述代码执行完后,applyMiddleware方法陆续获得了3个参数,第一个是middlewares数组[mid1, mid2, mid3,...],第二个是Redux原生的createStore方法,最后一个是reducer。然后我们可以看到applyMiddleware利用createStore和reducer创建了一个store。而store的getState方法和dispatch方法又分别被直接和间接地赋值给middlewareAPI变量的store

        const middleAPI = {
            getState: store.getState,
            dispatch: (action) => dispatch(action)
        }
        chain = middlewares.map(middle => middleware(middlewareAPI))
    

    然后,每个middleware带着middlewareAPI这个参数分别执行一遍,执行后,得到一个chain数组[f1, f2, ..., fx, ..., fn],它保存的对象是第二个箭头函数返回的匿名函数。因为是闭包,每个匿名函数多可以访问相同的store,即middlewareAPI.

    3.组合串联middleware

    这一层只有一行代码,确是applyMiddleware精华所在。

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

    其中,compose是函数式编程中的组合,它将chain中的所有匿名函数[f1, f2, ..., fn]组装成一个新的函数,即新的dispatch。当新的dispatch执行的时候,[f1, f2, ...]会从右到左依次执行。Redux中compose的实现是这样的,当然实现的方式不唯一。

       function compose(...funs) {
           return arg => funcs.reduceRight( (compose, f) => f(composed), arg)
       }
    
    compose(...funcs)返回的是一个匿名函数,其中funcs就是chain数组。当调用reduceRight时,依次从funcs数组的右端取一个函数f(x)拿来执行,f(x)的参数composed就是前一次f(x+1)执行的结果,而第一次执行的f(n)n代表chain的长度,它的参数arg就是store.dispatch。
    

    因此,当compose执行完后,我们得到的dispatch是这样的:

    假设n=3:
    dispatch = f1(f2(f3(store.dispatch)));

    这时调用dispatch,每一个middleware就会依次执行了。

    4.在middleware中调用dispatch会发生什么呢?

    经过compose后,所有的middleware就算是已经串联起来了。

    那么问题来了?

    在分发store时,我们有说到每个middleware都可以访问store,也就是我们说的通过middlewareAPI这个变量去拿到dispatch属性进行操作。那么如果在middleware中调用store.dispatch会如何?和调用next()有什么区别?

    先上一波代码:

        //next()
        const logger = store => next => action => {
            console.log('dispatch:', action);
            next(action);
            console.log('finish:', action );
        }
        
        //store.dispatch(action);
        const logger = store => next => action {
            console.log('dispatch:', action);
            store.dispatch(action);
            console.log('finishL:', action);
        }
    

    在分发store的时候,我们有说过:midddleware中的store的dispatch通过匿名函数的方式和最终compose结束后的新dispatch保持一致,所以,在middleware中调用store.dispatch()和在其他任何地方调用其实效果是一样的。

    而如果在middleware中调用next(),效果是进入下一个middleware中。

    具体如下两个图1和图2所示:

    如图1所示,正常情况下,当我们去分发一个action时,middleware会通过next(action)一层层处理和传递action直到redux原生的dispatch。如果某个middleware中使用了store.dispatch(action)来分发action,就会发生如图2所示的情况。这就相当于是又从头开始了。

    那么问题又来了,假如这个middleware一直简单粗暴地调用store.dispatch(action),就会形成一个无限循环了,那么store.dispatch(action)的用武之地到底在哪里呢?

    假如我们需要发送一个异步请求到服务端获取数据,成功后弹出个message,这里我们通常会用到reduce-thunk这个插件。

    const thunk = store => next => action => 
        typeof action === 'function' ? 
            action(store.dispatch, store.getState) :
            next(action)
    

    代码很清晰,就是会先判断你dispatch过来的action是不是一个function,如果是则执行action,如果不是则传递到下一个middleware。因此,我们可以这样设计action:

        const getMessage = (dispatch, getState) => {
            const url = 'http://xxx.json';
            Axios.get(url)
              .then( res => {
                  dispatch({
                      type: 'SHOW_MESSAGE',
                      message: res.data.data
                  })
              })
              .catch( err => {
                  dispatch({
                      type: 'ERR_GET',
                      message: 'error'
                  })
              }) 
        }
    
    如上所示,只要在应用中去调用store.dispatch(getThenShow), redux-thunk这个middleware接收到后就会去执行getMessage方法。getMessage会先请求数据,根据请求结果去做相对应的分发action。这这里的dispatch就是通过redux-thunk这个middleware传递进来的。
    
    总结:在middleware中使用dispatch的场景一般是接受到一个定向action,而这个action又并不希望到达原生的分发action,往往用在异步请求的需求里面。
    

    ----------------作者的话:其实看了挺多遍,在脑海中构建整个middleware流程,再结合上周学习Redux时的Demo才渐渐的知其形,还需要多在实践中会其神!--------------

  • 相关阅读:
    【SQL】通过Sql实现根据分组合并指定列内容的查询 SamWang
    [转]SQLServer中全文搜索与Like的差异分析
    【笔记】《C#高效编程改进C#代码的50个行之有效的办法》第1章C#语言习惯(1)属性的特性以及索引器(SamWang)
    [转]SqlServer全文索引实例
    [转]visual studio2005中文版无法打开项目文件:*.csproj,提示“此安装不支持该项目类型”的解决办法
    [转]李天平:程序人生成长发展中的一些感悟
    【改进】C# WinForm捕获全局异常 SamWang
    Windows 7 虚拟无线网卡(microserof virtual wifi miniport adapter)功能的使用
    VB.Net 串口通信示例
    VB.Net 串口通信用法
  • 原文地址:https://www.cnblogs.com/fe-linjin/p/10494763.html
Copyright © 2011-2022 走看看