<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解洋蔥模型

          共 6004字,需瀏覽 13分鐘

           ·

          2020-10-17 01:31


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

          前言

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

          話不多說,正文如下。

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

          先貼代碼如下。

          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));
          ????};
          ??});
          }

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

          接下來將提供幾個簡單 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");
          ??};
          }

          此時如果將以上 foo bar baz 三個中間件組合運行如下:

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

          以上將會在控制臺輸出什么?

          大家可以思考一下。

          ...

          熟悉中間件運行順序的同學可能很快得出答案:

          [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àn)在在 Web 社區(qū)發(fā)揮極大作用的 Redux、Express、Koa,開發(fā)者利用其中的洋蔥模型,構(gòu)建無數(shù)強大又有趣的 Web 應(yīng)用和 Node 應(yīng)用。更不用提基于這三個衍生出來的 Dva、Egg 等。所以其實需要理解的是這套實現(xiàn)機制原理,如果光是記住中間件執(zhí)行順序,未免太過無趣了,現(xiàn)在讓我們逐層逐層解構(gòu)以上代碼來探索洋蔥模型吧。

          到這里,正文正式開始!

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

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

          組合中間件機制

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

          以上 compose 傳入了 fooMiddleware、barMiddleware、bazMiddleware 三個中間件進行組合,內(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 就等于最終返回出來的 nextWrapper。

          (這里用了 #1,#2 用來指代不同組合的 nextWrapper,實際上并沒有這樣的語法,須知)

          img

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

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

          因為直到以下這一句,傳入 next 函數(shù)作為參數(shù),才開始真正的觸發(fā)了 nextWrapper,開始迭代執(zhí)行所有組合過的中間件。

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

          我們在上面得知了 chain 最終是形如(...args) => fooMiddleware(barMiddleware(bazMiddleware(...args)))的函數(shù)。因此當傳入 next 函數(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ù),其實并不是我們一開始的 next 函數(shù)了,而是 bazMiddleware 函數(shù)執(zhí)行后返回的 next_from_baz。barMiddleware 收到 next 參數(shù)開始執(zhí)行,打印日志后,返回了 next_from_bar 函數(shù)。
          3. 第三步,同理,fooMiddleware 函數(shù)接收的期望 next 參數(shù)是 barMiddleware 函數(shù)執(zhí)行后返回的 next_from_bar。fooMiddleware 收到 next 參數(shù)開始執(zhí)行,打印日志并返回了 next_from_foo 函數(shù)。

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

          再用示意圖詳細描述即為:

          img

          此時經(jīng)過以上步驟,控制臺輸出了下述日志:

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

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

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

          如果直接寫 Monkeypatching 如下

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

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

          Monkeypatching 本質(zhì)上是一種 hack。“將任意的方法替換成你想要的”。

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

          到這里我想出個考題,如下:

          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)));
          ??

          請問,chain(1) 輸出值?

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

          那么再問:

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

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

          再看如下代碼:

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

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

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

          洋蔥模型機制

          nextChain("{data}");
          ??

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

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

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

          img

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

          控制臺輸出日志:

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

          函數(shù)入棧直到最終的 next 函數(shù),我們知道,next 函數(shù)并沒有任何函數(shù)了,也就是說到達了終點。

          接下來就是逐層出棧。示意圖如下

          img

          控制臺輸出日志:

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

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

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

          img

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

          我們修改一開始的洋蔥模型,示例如下:

          img

          小結(jié)

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

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

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

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

          你能想到大致如何實現(xiàn)了么?是不是有點撥開云霧見太陽的感覺了?

          如果有,本文發(fā)揮了它的作用和價值,筆者將會不甚榮幸。如果沒有,那筆者的表達能力還是有待加強。

          ??愛心三連擊

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

          2.關(guān)注公眾號程序員成長指北,回復「1」加入Node進階交流群!「在這里有好多 Node 開發(fā)者,會討論 Node 知識,互相學習」!

          3.也可添加微信【ikoala520】,一起成長。


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

          瀏覽 59
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  中日韩免费视频 | 福利视频一区二区 | 天天干熟女 | 亚州的图五月丁香婷婷 | 青青草91久久久久久久久 |