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>

        深入理解洋蔥模型

        共 6004字,需瀏覽 13分鐘

         ·

        2020-10-17 01:31


        作者:掘金@蘇里? ?https://juejin.im/post/6844904025767280648

        前言

        本文來(lái)由,希望可以剖析中間件的組合原理,從而幫助大家更加理解洋蔥模型。

        話不多說(shuō),正文如下。

        這一段代碼來(lái)源于 redux 里導(dǎo)出的 compose 函數(shù)。我做了一些修改。主要是給匿名函數(shù)添加了名稱(chēng),比如 reducer 和 nextWrapper,主要原因是匿名函數(shù)(anonymous)不便于調(diào)試。所以 《You-Dont-Know-JS》 的作者 Kyle Simpson 大叔就對(duì)箭頭函數(shù)持保留意見(jiàn),認(rèn)為不該亂用,不過(guò)跑題了,扯回。

        先貼代碼如下。

        function?compose(...funcs)?{
        ??if?(funcs.length?===?0)?{
        ????return?arg?=>?arg;
        ??}

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

        ??return?funcs.reduce(function?reducer(a,?b)?{
        ????return?function?nextWrapper(...args)?{
        ??????return?a(b(...args));
        ????};
        ??});
        }

        接下來(lái)全文將基于此函數(shù)剖析。

        接下來(lái)將提供幾個(gè)簡(jiǎn)單 redux 中間件,同樣,我避免了箭頭函數(shù)的使用,理由同上。代碼如下:

        function?next(action)?{
        ??console.log("[next]",?action);
        }

        function?fooMiddleware(next)?{
        ??console.log("[fooMiddleware]?trigger");
        ??return?function?next_from_foo(action)?{
        ????console.log("[fooMiddleware]?before?next");
        ????next(action);
        ????console.log("[fooMiddleware]?after?next");
        ??};
        }

        function?barMiddleware(next)?{
        ??console.log("[barMiddleware]?trigger");
        ??return?function?next_from_bar(action)?{
        ????console.log("[barMiddleware]?before?next");
        ????next(action);
        ????console.log("[barMiddleware]?after?next");
        ??};
        }

        function?bazMiddleware(next)?{
        ??console.log("[bazMiddleware]?trigger");
        ??return?function?next_from_baz(action)?{
        ????console.log("[bazMiddleware]?before?next");
        ????next(action);
        ????console.log("[bazMiddleware]?after?next");
        ??};
        }

        此時(shí)如果將以上 foo bar baz 三個(gè)中間件組合運(yùn)行如下:

        const?chain?=?compose(fooMiddleware,?barMiddleware,?bazMiddleware);
        const?nextChain?=?chain(next);
        nextChain("{data}");

        以上將會(huì)在控制臺(tái)輸出什么?

        大家可以思考一下。

        ...

        熟悉中間件運(yùn)行順序的同學(xué)可能很快得出答案:

        [bazMiddleware]?trigger
        [barMiddleware]?trigger
        [fooMiddleware]?trigger
        [fooMiddleware]?before?next
        [barMiddleware]?before?next
        [bazMiddleware]?before?next
        [next]?{data}
        [bazMiddleware]?after?next
        [barMiddleware]?after?next
        [fooMiddleware]?after?next

        寫(xiě)不出正確答案的同學(xué)也無(wú)須灰心。這篇文章的目的,正是幫助大家更好理解這一套機(jī)制原理。

        這種洋蔥模型,也即是中間件的能力之強(qiáng)大眾所周知,現(xiàn)在在 Web 社區(qū)發(fā)揮極大作用的 Redux、Express、Koa,開(kāi)發(fā)者利用其中的洋蔥模型,構(gòu)建無(wú)數(shù)強(qiáng)大又有趣的 Web 應(yīng)用和 Node 應(yīng)用。更不用提基于這三個(gè)衍生出來(lái)的 Dva、Egg 等。所以其實(shí)需要理解的是這套實(shí)現(xiàn)機(jī)制原理,如果光是記住中間件執(zhí)行順序,未免太過(guò)無(wú)趣了,現(xiàn)在讓我們逐層逐層解構(gòu)以上代碼來(lái)探索洋蔥模型吧。

        到這里,正文正式開(kāi)始!

        以上代碼的靈魂之處在于 Array.prototype.reduce(),不了解此函數(shù)的同學(xué)強(qiáng)烈建議去 MDN 遛跶一圈 MDN | Array.prototype.reduce()。

        reduce 函數(shù)是函數(shù)式編程的一個(gè)重要概念,可用于實(shí)現(xiàn)函數(shù)組合(compose)

        組合中間件機(jī)制

        const?chain?=?compose(fooMiddleware,?barMiddleware,?bazMiddleware);
        ??

        以上 compose 傳入了 fooMiddleware、barMiddleware、bazMiddleware 三個(gè)中間件進(jìn)行組合,內(nèi)部執(zhí)行步驟可以分解為以下兩步。

        1. 第一步輸入?yún)?shù):a -> fooMiddleware,b -> barMiddleware

        執(zhí)行 reduce 第一次組合,得到返回輸出:function nextWrapper#1(...args) { return fooMiddleware(barMiddleware(...args)) }

        1. 第二步輸入?yún)?shù):a -> function nextWrapper#1(...args) { return fooMiddleware(barMiddleware(...args)) },b -> bazMiddleware

        執(zhí)行 reduce 第二次組合,得到返回輸出:function nextWrapper#2(...args) { return nextWrapper#1(bazMiddleware(...args)) }。

        所以 chain 就等于最終返回出來(lái)的 nextWrapper。

        (這里用了 #1,#2 用來(lái)指代不同組合的 nextWrapper,實(shí)際上并沒(méi)有這樣的語(yǔ)法,須知)

        img

        應(yīng)用中間件機(jī)制

        然而此時(shí)請(qǐng)留意,所有中間件并沒(méi)有執(zhí)行,到目前為止最終通過(guò)高階函數(shù) nextWrapper 返回了出來(lái)而已。

        因?yàn)橹钡揭韵逻@一句,傳入 next 函數(shù)作為參數(shù),才開(kāi)始真正的觸發(fā)了 nextWrapper,開(kāi)始迭代執(zhí)行所有組合過(guò)的中間件。

        const?nextChain?=?chain(next);
        ??

        我們?cè)谏厦娴弥?chain 最終是形如(...args) => fooMiddleware(barMiddleware(bazMiddleware(...args)))的函數(shù)。因此當(dāng)傳入 next 函數(shù)時(shí),內(nèi)部執(zhí)行步驟可以分為下述幾步:

        1. 第一步,執(zhí)行 chain 函數(shù)(也即是 nextWrapper#2),從 compose 的函數(shù)組合從內(nèi)至外,next 參數(shù)首先交由 bazMiddleware 函數(shù)執(zhí)行,打印日志后,返回了函數(shù) next_from_baz。
        2. 第二步,next_from_baz 立即傳入 nextWrapper#1,返回了 fooMiddleware(barMiddleware(...args))。因此,barMiddleware 函數(shù)接收的期望 next 參數(shù),其實(shí)并不是我們一開(kāi)始的 next 函數(shù)了,而是 bazMiddleware 函數(shù)執(zhí)行后返回的 next_from_baz。barMiddleware 收到 next 參數(shù)開(kāi)始執(zhí)行,打印日志后,返回了 next_from_bar 函數(shù)。
        3. 第三步,同理,fooMiddleware 函數(shù)接收的期望 next 參數(shù)是 barMiddleware 函數(shù)執(zhí)行后返回的 next_from_bar。fooMiddleware 收到 next 參數(shù)開(kāi)始執(zhí)行,打印日志并返回了 next_from_foo 函數(shù)。

        所以此時(shí)我們此時(shí)可知,運(yùn)行完 chain 函數(shù)后,實(shí)際上 nextChain 函數(shù)就是 next_from_foo 函數(shù)。

        再用示意圖詳細(xì)描述即為:

        img

        此時(shí)經(jīng)過(guò)以上步驟,控制臺(tái)輸出了下述日志:

        [bazMiddleware]?trigger
        [barMiddleware]?trigger
        [fooMiddleware]?trigger
        ??

        這里的 next_from_baz,next_from_bar,next_from_foo 其實(shí)就是一層層的對(duì)傳入的參數(shù)函數(shù) next 包裹。官方說(shuō)法稱(chēng)之為 Monkeypatching。

        我們很清晰的知道,next_from_foo 包裹了 next_from_bar,next_from_bar 又包裹了 next_from_baz,next_from_baz 則包裹了 next。

        如果直接寫(xiě) Monkeypatching 如下

        const?prevNext?=?next;
        next?=?(...args)?=>?{
        ??//?@todo
        ??prevNext(...args);
        ??//?@todo
        };
        ??

        但這樣如果需要 patch 很多功能,我們需要將上述代碼重復(fù)許多遍。的確不是很 DRY。

        Monkeypatching 本質(zhì)上是一種 hack?!皩⑷我獾姆椒ㄌ鎿Q成你想要的”。

        關(guān)于 Monkeypatching 和 redux 中間件的介紹,十分推薦閱讀官網(wǎng)文檔 Redux Docs | Middleware。

        到這里我想出個(gè)考題,如下:

        function?add5(x)?{
        ??return?x?+?5;
        }

        function?div2(x)?{
        ??return?x?/?2;
        }

        function?sub3(x)?{
        ??return?x?-?3;
        }

        const?chain?=?[add5,?div2,?sub3].reduce((a,?b)?=>?(...args)?=>?a(b(...args)));
        ??

        請(qǐng)問(wèn),chain(1) 輸出值?

        執(zhí)行順序?yàn)?sub3 -> div2 -> add5。(1 - 3) / 2 + 5 = 4。答案是 4。

        那么再問(wèn):

        const?chain?=?[add5,?div2,?sub3].reduceRight((a,?b)?=>?(...args)?=>?b(a(...args)));
        ??

        此時(shí) chain(1) 輸出值?還是 4。

        再看如下代碼:

        const?chain?=?[add5,?div2,?sub3].reverse().reduce((a,?b)?=>?(...args)?=>?b(a(...args)));
        ??

        此時(shí) chain(1) 輸出值?仍然是 4。

        如果你對(duì)上述示例都能很清晰的運(yùn)算出答案,那么你應(yīng)該對(duì)上文中 chain(next)的理解 ok,那么請(qǐng)繼續(xù)往下看。

        洋蔥模型機(jī)制

        nextChain("{data}");
        ??

        終于重頭戲來(lái)了,nextChain 函數(shù)來(lái)之不易,但毫無(wú)疑問(wèn),它的能力是十分強(qiáng)大的。(你看,其實(shí)在 redux 中,這個(gè) nextChain 函數(shù)其實(shí)就是 redux 中的 dispatch 函數(shù)。)

        文章截止目前為止,我們得知了 nextChain 函數(shù)即為 next_from_foo 函數(shù)。

        因此下述的執(zhí)行順序我將用函數(shù)堆棧圖給大家示意。

        img

        依次執(zhí)行,每當(dāng)執(zhí)行到 next 函數(shù)時(shí),新的 next 函數(shù)入棧,循環(huán)往復(fù),直到 next_from_baz 為止。函數(shù)入棧的過(guò)程,就相當(dāng)于進(jìn)行完了洋蔥模型從外到里的進(jìn)入過(guò)程。

        控制臺(tái)輸出日志:

        [fooMiddleware]?before?next
        [barMiddleware]?before?next
        [bazMiddleware]?before?next
        ??

        函數(shù)入棧直到最終的 next 函數(shù),我們知道,next 函數(shù)并沒(méi)有任何函數(shù)了,也就是說(shuō)到達(dá)了終點(diǎn)。

        接下來(lái)就是逐層出棧。示意圖如下

        img

        控制臺(tái)輸出日志:

        [next]?{data}
        [bazMiddleware]?after?next
        [barMiddleware]?after?next
        [fooMiddleware]?after?next
        ??

        函數(shù)出棧的過(guò)程,就相當(dāng)于洋蔥模型從里到外的出去過(guò)程。

        上述是函數(shù)堆棧的執(zhí)行順序。而下述示意圖是我整理后幫助大家理解的線性執(zhí)行順序。每當(dāng)執(zhí)行到 next(action)的時(shí)候函數(shù)入棧,原 next 函數(shù)暫時(shí)停止執(zhí)行,執(zhí)行新的 next 函數(shù),正如下圖彎曲箭頭所指。

        img

        上圖,代碼從上至下運(yùn)行,實(shí)際上就是調(diào)用棧的一個(gè)程序控制流程。所以理論上無(wú)論有多少個(gè)函數(shù)嵌套,都可以等同理解。

        我們修改一開(kāi)始的洋蔥模型,示例如下:

        img

        小結(jié)

        redux 的中間件也就是比上述示例的中間件多了一層高階函數(shù)用以獲取框架內(nèi)部的 store。

        const?reduxMiddleware?=?store?=>?next?=>?action?=>?{
        ??//?...
        ??next(action);
        ??//?...
        };
        ??

        而 koa 的中間件多了 ctx 上下文參數(shù),和支持異步。

        app.use(async?(ctx,?next)?=>?{
        ??//?...
        ??await?next();
        ??//?...
        });
        ??

        你能想到大致如何實(shí)現(xiàn)了么?是不是有點(diǎn)撥開(kāi)云霧見(jiàn)太陽(yáng)的感覺(jué)了?

        如果有,本文發(fā)揮了它的作用和價(jià)值,筆者將會(huì)不甚榮幸。如果沒(méi)有,那筆者的表達(dá)能力還是有待加強(qiáng)。

        ??愛(ài)心三連擊

        1.看到這里了就點(diǎn)個(gè)在看支持下吧,你的點(diǎn)贊在看是我創(chuàng)作的動(dòng)力。

        2.關(guān)注公眾號(hào)程序員成長(zhǎng)指北,回復(fù)「1」加入Node進(jìn)階交流群!「在這里有好多 Node 開(kāi)發(fā)者,會(huì)討論 Node 知識(shí),互相學(xué)習(xí)」!

        3.也可添加微信【ikoala520】,一起成長(zhǎng)。


        “在看轉(zhuǎn)發(fā)”是最大的支持

        瀏覽 59
        點(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>
            大鸡巴天天操 | 人妖欧美一区二区三区 | 精油按摩一级片 | 亚洲av免费网站 婷婷激情网站 | 91精品国自产拍一区二区91在 | 国产自产精品 | 国产成人无码电影 | www.色色色com | 91探花秘 在线播放偷拍 | wwwav 中文字幕第九页 |