深度剖析 redux applyMiddleware 中 compose 構建異步數據流的思路

前言

本作者站在自己的角度深入淺出...算了別這麼裝逼分析 redux applyMiddleware 在設計過程中通過 conpose 構建異步數據流的思路。自己假設的一些場景幫助理解,希望大家在有異步數據流並且使用redux的過程中能夠有自己的思路(脫離thunk or saga)構建自己的 enhance.如果你看完本文之後還想對我有更多的瞭解,可以移步我的github

言歸正傳,既然是解決異步問題那麼我們就給自己設立一個小場景吧,請問下面這個函數數組,如何讓其順序調用。

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] );// 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具有兩個特性:
    1. 能接受一個函數作爲參數,這個函數還能接收另一個函數作爲參數,並會在某個時刻進行調用
    1. 每次循環都會根據當前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
那咱們的功能就此實現了;不過可以優化一哈。咱們上面的代碼有幾個要素:
  1. 數組循環
  2. 狀態傳遞
  3. 初始狀態爲數組的第一個元素
  4. 最終需要拿到單一的返回值
不就是活脫脫用來描述reduce的嗎?於是乎我們可以這樣擼
    //pre 前一個狀態、 cur當前循環函數、next 待接收的下一個
      fucArr.reduce((pre, cur)=>{
          return (next)=>pre(next=>()=>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)
    }
   看上去那是相當的完美,根據咱們寫代碼的思路咱們來比對一下原版吧。
  1. length === 0 時: 返回一個傳入什麼返回什麼的函數。
  2. length === 1 時: 直接返回傳入函數函數。
  3. 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狀態進行等等,如果大家有興趣可以自己實現一下,爲了不把大家的思路帶歪,在寫的過程中並沒有體現出來。
   如果覺得我寫對你有一定的幫助,那就點個贊吧,因爲您的鼓勵是我最大的動力。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章