深度剖析 redux applyMiddleware 中 compose 構建非同步資料流的思路

菜lee發表於2019-02-16

前言

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

正文

實際場景中遇到一個這樣的問題:商品詳情頁的微信頁面,未註冊的使用者點選購買一個商品,我們希望能夠實現靜默登入就有如下幾個步驟:

  1. 獲取code;
  2. 獲取openId、AccessToken;
  3. 根據openId、獲取openId、AccessToken;獲取使用者資訊實現自動註冊然後登入;
  4. 跳到商品購買頁。

這是就是一個典型非同步資料流的過程。在上一個函式執行到某個時候再去呼叫下一個函式,使得這些個函式能夠順序執行。我們簡化一下,構建如下的函式陣列使得他們能夠順序執行吧:

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具有兩個特性:
    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(()=>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狀態…,
  • 如果大家有興趣可以自己實現一下,為了不把大家的思路帶歪,在寫的過程中並沒有體現出來。

感謝@MrTreasure幫我指出文章中的問題,如果覺得我寫對你有一定的幫助,那就點個贊吧,因為您的鼓勵是我最大的動力。

相關文章