1. <strong id="7actg"></strong>
    2. <table id="7actg"></table>

    3. <address id="7actg"></address>
      <address id="7actg"></address>
      1. <object id="7actg"><tt id="7actg"></tt></object>

        從koa/redux看如何設(shè)計(jì)中間件

        共 6779字,需瀏覽 14分鐘

         ·

        2021-11-09 22:13

        導(dǎo)語(yǔ) 本文學(xué)習(xí)優(yōu)秀庫(kù)koa/redux如何設(shè)計(jì)中間件

        大廠技術(shù)??高級(jí)前端??Node進(jìn)階

        點(diǎn)擊上方?程序員成長(zhǎng)指北,關(guān)注公眾號(hào)

        回復(fù)1,加入高級(jí)Node交流群

        或許你在學(xué)習(xí)koa/redux時(shí),經(jīng)常會(huì)聽到中間件這個(gè)詞,大概也知道它們通過(guò)這種設(shè)計(jì)模式,使得自定義的中間件(函數(shù))能正確在插入到上下文環(huán)境中執(zhí)行。那它們究竟是怎么實(shí)現(xiàn)的呢?本文僅探討koa/redux是如何設(shè)計(jì)中間件。中間件是一種實(shí)現(xiàn)「關(guān)注點(diǎn)分離」的設(shè)計(jì)模式,該模式有兩個(gè)特點(diǎn):

        • 中間件middle是一個(gè)函數(shù)

        • middle有個(gè)next參數(shù),也是函數(shù),代表下個(gè)要執(zhí)行的中間件。

        function m1(next) {  console.log("m1");  next();  console.log("v1");}
        function m2(next) { console.log("m2"); next(); console.log("v2");}
        function m3() { console.log("m3");}


        如上所示:中間件 m1->m2->m3執(zhí)行,打印結(jié)果為 m1->m2->m3->v2->v1。這種模式有個(gè)形象的名字,洋蔥模型。但現(xiàn)在我們暫時(shí)忘記這些名字,就想想如何實(shí)現(xiàn)中間件(函數(shù))的聯(lián)動(dòng)吧。有兩種思路,第一是遞歸;第二是鏈?zhǔn)秸{(diào)用。

        遞歸

        設(shè)置一個(gè)數(shù)組按順序存儲(chǔ)函數(shù),根據(jù) index 值,按順序一個(gè)個(gè)執(zhí)行,如下:

        const middles = [m1, m2, m3];
        function compose(arr) { function dispath(index) { if (index === arr.length) return; const route = arr[index]; const next = () => dispath(index + 1); // 遞歸執(zhí)行數(shù)組中下一個(gè)函數(shù) return route(next); } dispath(0);}
        compose(middles); // 打印m1 -> m2 -> m3 -> v2 -> v1


        鏈?zhǔn)秸{(diào)用

        將函數(shù)當(dāng)作成參數(shù)傳給上一個(gè)中間件,這樣前一個(gè)中間件執(zhí)行完就可以執(zhí)行下一個(gè)中間件。

        1、直接調(diào)用:

        m1(() => m2(() => m3())); // 打印m1 -> m2 -> m3 -> v2 -> v1// m2的參數(shù)next是 () => m3(),// m1的參數(shù)next是 () => m2(() => m3())

        此種方法雖然可行,但是 m1,m2,m3 都是寫死的,不是公共方法。

        2、構(gòu)建next的函數(shù)createFn:

        我們觀察到在傳遞參數(shù)時(shí),m3 和 m2 都變成函數(shù)再傳入,那這個(gè)變成函數(shù)的過(guò)程是否能提?。喝缦?,參數(shù) middle 是中間件,參數(shù) next 是接下來(lái)要執(zhí)行的函數(shù)。轉(zhuǎn)換后 next 變成 middle 的參數(shù)。

        function createFn(middle, next) {  return function() {    middle(next);  };}
        // 需要先將后面的中間件變成我們需要的 next 函數(shù):const fn3 = createFn(m3, null);const fn2 = createFn(m2, fn3);const fn1 = createFn(m1, fn2);
        fn1(); // 打印m1 -> m2 -> m3 -> v2 -> v1

        這里 fn3/fn2/fn1 也是固定的,但我們看出這些中間狀態(tài)變量,可以隱藏掉,統(tǒng)一用 next 代替:

        let next = () => {};
        next = createFn(m3, null);next = createFn(m2, next);next = createFn(m1, next);
        next(); // 打印m1 -> m2 -> m3 -> v2 -> v1

        優(yōu)化如下:

        let next = () => {};// 倒序for (let i = middles.length; i >= 0; i--) {  next = createFn(middles[i], next);}
        next(); // 打印m1 -> m2 -> m3 -> v2 -> v1

        3、redux 的 reduceRight

        仔細(xì)觀察上面這種倒序,且每次拿上次的值進(jìn)行計(jì)算的方法,是不是很像 reduceRight。(好吧,或許我們看不出來(lái),但是早期 redux 就是這么實(shí)現(xiàn)的,我們直接拿過(guò)來(lái)研究):

        const middles = [m1, m2, m3];
        function compose(arr) { return arr.reduceRight( (a, b) => { // b是middle,a是next, return () => b(a); // 每次返回的是一個(gè)函數(shù),執(zhí)行這個(gè)函數(shù)為middle(next),即b(a) }, () => {} // 初始化的a值,空函數(shù) );}
        const mid = compose(middles);mid(); // 打印m1 -> m2 -> m3 -> v2 -> v1

        4、redux 的 reduce

        const middles = [m1, m2, m3];
        function compose(arr) { return arr.reduce((a, b) => { return (...arg) => a(() => b(...arg)); // a 是 next函數(shù),b是middle函數(shù) });}
        const mid = compose(middles);mid(); // 打印m1 -> m2 -> m3 -> v2 -> v1

        改成這種正序的方式,反而不好理解。嘗試解釋一下:a 是 next 函數(shù),b 是 middle 函數(shù)。(...arg) => a(() => b(...arg)) 這簡(jiǎn)直就是我們最初這種寫法 m1(() => m2(() => m3())) 的直接映射。摘抄一下這篇參考一作者的解釋,感興趣的同學(xué)可自行推導(dǎo)一下:

        // 第 1 次 reduce 的返回值,下一次將作為 aarg => fn1(() => fn2(arg));
        // 第 2 次 reduce 的返回值,下一次將作為 aarg => (arg => fn1(() => fn2(arg)))(() => fn3(arg));
        // 等價(jià)于...arg => fn1(() => fn2(() => fn3(arg)));
        // 執(zhí)行最后返回的函數(shù)連接中間件,返回值等價(jià)于...fn1(() => fn2(() => fn3(() => {})));

        明白reduceRight到reduce轉(zhuǎn)換不是最關(guān)鍵的,關(guān)鍵的是明白上面幾種寫法讓我們能鏈?zhǔn)秸{(diào)用函數(shù)。

        傳遞參數(shù)

        設(shè)計(jì)一個(gè)中間件模式,怎么能少得了參數(shù)的傳遞。我們先想想如何組織我們中間件:很明顯,我們通過(guò) next 執(zhí)行下個(gè)中間件,那么傳值給下個(gè)中間件就是給 next 添加參數(shù):

        function m1(next) {  console.log("m1");  next("v2"); // 將'v2'傳給下個(gè)中間件m2}

        那么 m2 該怎么獲取這個(gè)值呢?因?yàn)?next 代表 m2 執(zhí)行后的值,next 傳遞參數(shù)就是說(shuō) m2 需要返回函數(shù),該函數(shù)的參數(shù)就是傳遞的值,如下:

        function m2(next) {  return function(action) {  // 這個(gè)action就是上一個(gè)函數(shù)傳來(lái)的'v2'    next(action);  };}

        這種寫法等價(jià)于:

        const m2 = next => action => {  next(action);};

        所以按照上面這種方式組織我們的中間件,我們就既能鏈?zhǔn)綀?zhí)行又能傳遞參數(shù)。如下:

        const m1 = next => action => {  console.log("m1", action);  next(action);};
        const m2 = next => action => { console.log("m2", action); next(action);};
        const m3 = next => action => { console.log("m3", action);};

        那我們?nèi)绾螌?shí)現(xiàn)呢?

        1、直接調(diào)用:

        m1(arg => m2(() => m3()(arg))(arg))("666");// 打?。簃1,m2,m3都打印666

        2、創(chuàng)建createFn函數(shù):

        createFn返回的函數(shù)添加了參數(shù)action,代表了中間件之間的參數(shù)。

        // 我們給返回的函數(shù)加上參數(shù)action并執(zhí)行function createFn(middle, next) {  return function (action) {    middle(next)(action);  }}
        const middles = [m1, m2, m3];
        let next = () => {};for (let i = middles.length - 1; i >= 0; i--) { next = createFn(middles[i], next);}
        next("666"); // 打?。簃1,m2,m3都打印666

        3、 redux 的 reduceRight 與 reduce:

        返回的結(jié)果直接執(zhí)行,因?yàn)槲覀兗恿艘粚臃祷睾瘮?shù)

        const middles = [m1, m2, m3];
        function compose(arr) { return arr.reduceRight( (a, b) => b(a), // 注意這里,上個(gè)版本返回的是函數(shù)() => b(a);這個(gè)版本變成b(a),直接執(zhí)行了,原因是我們中間件返回函數(shù),所以這里需要將其執(zhí)行 () => {} );}
        const mid = compose(middles);mid("666"); // 打?。簃1,m2,m3都打印666

        共同的屬性

        現(xiàn)在我們完成了中間件的鏈?zhǔn)秸{(diào)用和參數(shù)傳遞,已完成一個(gè)簡(jiǎn)單的中間件。但是如果我們這里不是普通的中間價(jià),而是 redux 的中間件。我們想要這些中間件都擁有一個(gè)初始化的 store,該如何處理呢?熟悉 redux 的朋友肯定知道中間件最后寫成這樣:

        const m1 = store => next => action => {  console.log("store1", store);  next(action);};
        const m2 = store => next => action => { console.log("store2", store); next(action);};
        const m3 = store => next => action => { console.log("store3", store);};

        我們還是按照上面幾個(gè)步驟來(lái)實(shí)現(xiàn)一下,最后講講為什么能這么設(shè)計(jì):

        1、 直接調(diào)用

        const store = { name: "redux" };
        // 基本寫法,我們將參數(shù)傳給每個(gè)中間件m1(arg => m2(() => m3()(arg))(arg))(store);

        2. 中間件先執(zhí)行一遍將 store 傳入進(jìn)去

        const store = { name: "redux" };
        const middles = [m1, m2, m3];
        const middlesWithStore = middles.map(middle => middle(store)); // 這里執(zhí)行了第一遍,將store傳進(jìn)來(lái)
        function createFn(middle, next) { return action => middle(next)(action);}
        let next = () => () => {};for (let i = middlesWithStore.length - 1; i >= 0; i--) { next = createFn(middlesWithStore[i], next);}
        next(store); // 打?。簊tore1,store2,store3 { name: 'redux' }

        3、 reduceRight 和 reduce :

        const store = { name: "redux" };
        const middles = [m1, m2, m3];
        const middlesWithStore = middles.map(middle => middle(store)); // 這里執(zhí)行了第一遍,將store傳進(jìn)來(lái)
        function compose(arr) { return arr.reduce((a, b) => (...args) => a(b(...args)));}
        const mid = compose(middlesWithStore)();mid(store); // 打?。簊tore1,store2,store3 { name: 'redux' }

        這里看起來(lái)簡(jiǎn)單,就是先執(zhí)行一遍中間件,但為什么可以先執(zhí)行一次函數(shù)將數(shù)據(jù)(store)傳進(jìn)去?而且這個(gè)數(shù)據(jù)在后來(lái)的調(diào)用中能被訪問(wèn)到?這背后涉及到的基礎(chǔ)知識(shí)是函數(shù)柯里化和閉包:

        柯里化與閉包

        1、柯里化

        柯里化是使用匿名單參數(shù)函數(shù)來(lái)實(shí)現(xiàn)多參數(shù)函數(shù)的方法。

        const m1 = store => next => action => {  console.log("store1", store);  next(action);};

        上面這種寫法,我們說(shuō)是將中間件 m1 柯里化了,它的特點(diǎn)是每次只傳一個(gè)參數(shù),返回的是新的函數(shù)。返回新函數(shù)這個(gè)特點(diǎn)很重要,因?yàn)楹瘮?shù)可以在其他地方再調(diào)用,所以本來(lái)一個(gè)連續(xù)的動(dòng)作被打斷了,變成了可以延遲執(zhí)行,也可以稱為參數(shù)前置。當(dāng)我們執(zhí)行:

        const middlesWithStore = middles.map(middle => middle(store));

        相當(dāng)于給每個(gè)中間件都添加了 store 屬性,而且返回的是函數(shù),可以等到你需要用它的時(shí)候再去使用。這就是柯里化的好處。

        2、閉包

        閉包:函數(shù)與其自由變量組成的環(huán)境,自由變量指不存在函數(shù)內(nèi)部的變量。當(dāng)函數(shù)按照值傳遞的方式在其他地方被調(diào)用時(shí),產(chǎn)生了閉包。

        上面的 m1 可以寫成下面這種格式,可以知道柯里化中間函數(shù)處于同一閉包,所以盡管我們是在其他地方調(diào)用了 next(action),但還是保存了最開始初始化的作用域,實(shí)現(xiàn)了真正的函數(shù)分開執(zhí)行。

          return function(next) {    return function(action) {      console.log("store1", store);      next(action);    };  };}

        總結(jié)

        可以說(shuō)我們整個(gè)中間件的設(shè)計(jì)就是建構(gòu)在返回函數(shù)形成閉包這種柯里化特性上。它讓我們緩存參數(shù),分開執(zhí)行,鏈?zhǔn)絺鬟f參數(shù)調(diào)用。所以 redux 中能提前注入 store,能有效傳遞 action。可以說(shuō)koa/redux的中間件機(jī)制是閉包/柯里化的經(jīng)典的實(shí)例。

        參考資料

        https://juejin.im/post/5bbdcf05e51d450e6c750693

        https://zhuanlan.zhihu.com/p/35040744

        https://zhuanlan.zhihu.com/p/20597452

        https://github.com/brickspert/blog/issues/22

        Node 社群


        我組建了一個(gè)氛圍特別好的 Node.js 社群,里面有很多 Node.js小伙伴,如果你對(duì)Node.js學(xué)習(xí)感興趣的話(后續(xù)有計(jì)劃也可以),我們可以一起進(jìn)行Node.js相關(guān)的交流、學(xué)習(xí)、共建。下方加 考拉 好友回復(fù)「Node」即可。


        ???“分享、點(diǎn)贊、在看” 支持一波??


        瀏覽 25
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        1. <strong id="7actg"></strong>
        2. <table id="7actg"></table>

        3. <address id="7actg"></address>
          <address id="7actg"></address>
          1. <object id="7actg"><tt id="7actg"></tt></object>
            好爽视频| 快播黄色下载 | 极品美女a诱v惑在线观看免费 | 抽插久久| 国精产品一区一区三区四川 | 人人摸人人爱 | 99天堂网 | 美日韩中文字幕 | 无码毛片一区二区三区人口 | 成人看兔费的黄片 |