<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>

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

          共 6779字,需瀏覽 14分鐘

           ·

          2021-11-09 22:13

          導語 本文學習優(yōu)秀庫koa/redux如何設(shè)計中間件

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

          點擊上方?程序員成長指北,關(guān)注公眾號

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

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

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

          • middle有個next參數(shù),也是函數(shù),代表下個要執(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。這種模式有個形象的名字,洋蔥模型。但現(xiàn)在我們暫時忘記這些名字,就想想如何實現(xiàn)中間件(函數(shù))的聯(lián)動吧。有兩種思路,第一是遞歸;第二是鏈式調(diào)用。

          遞歸

          設(shè)置一個數(shù)組按順序存儲函數(shù),根據(jù) index 值,按順序一個個執(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ù)組中下一個函數(shù) return route(next); } dispath(0);}
          compose(middles); // 打印m1 -> m2 -> m3 -> v2 -> v1


          鏈式調(diào)用

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

          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ù)時,m3 和 m2 都變成函數(shù)再傳入,那這個變成函數(shù)的過程是否能提取:如下,參數(shù) middle 是中間件,參數(shù) next 是接下來要執(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

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

          const middles = [m1, m2, m3];
          function compose(arr) { return arr.reduceRight( (a, b) => { // b是middle,a是next, return () => b(a); // 每次返回的是一個函數(shù),執(zhí)行這個函數(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)) 這簡直就是我們最初這種寫法 m1(() => m2(() => m3())) 的直接映射。摘抄一下這篇參考一作者的解釋,感興趣的同學可自行推導一下:

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

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

          傳遞參數(shù)

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

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

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

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

          這種寫法等價于:

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

          所以按照上面這種方式組織我們的中間件,我們就既能鏈式執(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)绾螌崿F(xiàn)呢?

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

          m1(arg => m2(() => m3()(arg))(arg))("666");// 打印:m1,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"); // 打印:m1,m2,m3都打印666

          3、 redux 的 reduceRight 與 reduce:

          返回的結(jié)果直接執(zhí)行,因為我們加了一層返回函數(shù)

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

          共同的屬性

          現(xiàn)在我們完成了中間件的鏈式調(diào)用和參數(shù)傳遞,已完成一個簡單的中間件。但是如果我們這里不是普通的中間價,而是 redux 的中間件。我們想要這些中間件都擁有一個初始化的 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);};

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

          1、 直接調(diào)用

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

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

          const store = { name: "redux" };
          const middles = [m1, m2, m3];
          const middlesWithStore = middles.map(middle => middle(store)); // 這里執(zhí)行了第一遍,將store傳進來
          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); // 打印:store1,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傳進來
          function compose(arr) { return arr.reduce((a, b) => (...args) => a(b(...args)));}
          const mid = compose(middlesWithStore)();mid(store); // 打印:store1,store2,store3 { name: 'redux' }

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

          柯里化與閉包

          1、柯里化

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

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

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

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

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

          2、閉包

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

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

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

          總結(jié)

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

          參考資料

          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 社群


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


          ???“分享、點贊在看” 支持一波??


          瀏覽 25
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

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

          手機掃一掃分享

          分享
          舉報
          <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>
                  99国产在线观看免费 | 精品日产乱码久久久 | 亚洲第一视频欧美在线视频 | 四虎综合| 婷婷五月天激情视频 |