中文字幕av专区_日韩电影在线播放_精品国产精品久久一区免费式_av在线免费观看网站

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Immutable.js到Redux函數式編程源碼分析

發布時間:2023-04-03 15:29:58 來源:億速云 閱讀:128 作者:iii 欄目:開發技術

這篇文章主要介紹了Immutable.js到Redux函數式編程源碼分析的相關知識,內容詳細易懂,操作簡單快捷,具有一定借鑒價值,相信大家閱讀完這篇Immutable.js到Redux函數式編程源碼分析文章都會有所收獲,下面我們一起來看看吧。

    基本概念

    函數式編程(英語:functional programming)或稱函數程序設計、泛函編程,是一種編程范式。它將電腦運算視為函數運算,并且避免使用程序狀態以及易變對象。其中,λ 演算為該語言最重要的基礎。而且,λ 演算的函數可以接受函數作為輸入參數和輸出返回值。

    以上是維基百科對于函數式編程的定義,用簡單的話總結就是“強調以函數使用為主的軟件開發風格”。

    在抽象的定義之外,從實際出發,JS 的函數式編程有以下幾個特點:

    • 函數是一等公民

    • 擁抱純函數,拒絕副作用

    • 使用不可變值

    函數式編程要素

    函數是一等公民

    我們經常聽到這句話,”在 JS 中函數是一等公民“,其具體的含義是,函數具有以下特征:

    • 可以被當作參數傳遞給其他函數

    • 可以作為另一個函數的返回值

    • 可以被賦值給一個變量

    函數式一等公民的特點是所有函數式編程語言所必須具有的,另一個必備特點則是支持閉包(上面的第二點其實很多時候都利用了閉包)

    純函數

    有且僅有顯示數據流:

    • 輸入:參數

    • 輸出:返回值

    一個函數要是純函數,要符合以下幾點:

    函數內部不能有副作用

    對于同樣的輸入(參數),必定得到同樣的輸出。

    這意味著純函數不能依賴外部作用域的變量

    副作用

    參考純函數“僅有顯示數據流”的定義,副作用的定義即擁有“隱式數據流”。或者說:

    • 會對函數作用域之外的執行上下文、宿主環境產生影響,如修改全局變量

    • 依賴了隱式輸入,如使用全局變量

    • 進行了與外界的隱式數據交換,如網絡請求

    不可變值

    當函數參數為引用類型時,對參數的改變將作用將映射到其本身。

    const arr = [1, 2, 3];
    const reverse = (arr) => {
      arr.reverse();
    };
    reverse(arr);
    console.log(arr); // [3,2,1]

    這種操作符合“副作用”的定義:修改了外部變量。破壞了純函數的顯示數據流。

    如果真的需要設計對數據的修改,則應該:

    • 拷貝原始數據

    • 修改拷貝結果,返回新的數據

    const reverse = (arr) => {
      const temp = JSON.parse(JSON.stringify(arr));
      return temp.reverse();
    };
    arr = reverse(arr);

    拷貝帶來的問題

    通過拷貝實現對外部數據的只讀直觀且簡單,代價則是性能

    對于一個大對象,每次的修改可能只是其中的一個屬性,那么每次的拷貝會帶來大量的冗余操作。當數據規模大,操作頻率高時,會帶來嚴重的性能問題。

    解決拷貝的性能問題: 持久化數據結構

    拷貝模式的問題根源在于:一個大對象只有一小部分有改變,卻要對整個對象做拷貝。

    這個情況其實和另一個場景很相似,就是 Git。一個項目有很多文件,但我一次可能只修改了其中一個。那么我本次的提交記錄是怎樣的呢?其處理邏輯就是:將改變部分和不變部分進行分離。

    **Git 快照保存文件索引,而不會保存文件本身。變化的文件將擁有新的存儲空間+新的索引,不變的文件將永遠呆在原地。**而在持久化數據結構中,則是變化的屬性的索引,和不變的屬性的索引

    持久化數據結構最常用的庫是 Immutable.js,其詳解見下文。

    JS 中三種編程范式

    JS 是一種多范式語言,而從前端的發展歷史來看,各時段的主流框架,也正對應了三種編程范式:

    • JQuery:命令式編程

    • React 類組件:面向對象

    • React Hooks、 Vue3:函數式編程

    函數式編程的優缺點

    優點

    • 利于更好的代碼組織。因為純函數不依賴于上下文所以天然具有高內聚低耦合的特點

    • 利于邏輯復用。純函數的執行是與上下文無關的,因此可以更好的在不同場景中復用

    • 便于單元測試。純函數對于相同輸入一定得到相同輸出的特點,便于自動化測試

    缺點

    • 相比于命令式編程,往往會包裝更多的方法,產生更多的上下文切換帶來的開銷。

    • 更多的使用遞歸,導致更高的內存開銷。

    • 為了實現不可變數據,會產生更多的對象,對垃圾回收的壓力更大。

    偏函數

    偏函數的定義簡單來說就是,將函數轉換為參數更少的函數,也就是為其預設參數。

    從 fn(arg1, arg2) 到 fn(arg1)

    柯里化(curry)函數

    柯里化函數在偏函數的基礎上,不僅減少了函數入參個數,還改變了函數執行次數。其含義就是將一個接收 N 個入參的函數,改寫為接受一個入參,并返回接受剩余 N-1 個參數的函數。也就是:

    fn(1,2,3) => fn(1)(2)(3)

    實現一個柯里化函數也是面試高頻內容,其實如果規定了函數入參個數,那么是很容易實現的。例如對于入參個數為 3 的函數,實現如下

    const curry = (fn) => (arg1) => (arg2) => (arg3) => fn(arg1, arg2, arg3);
    const fn = (a, b, c) => console.log(a, b, c);
    curry(fn)(1)(2)(3); // 1 2 3

    那么實現通用的 curry 函數的關鍵就在于:

    • 自動判斷函數入參

    • 自我遞歸調用

    const curry = (fn) => {
      const argLen = fn.length; // 原函數的入參個數
      const recursion = (args) =>
        args.length >= argLen
          ? fn(...args)
          : (newArg) => recursion([...args, newArg]);
      return recursion([]);
    };

    compose & pipe

    compose 和 pipe 同樣是很常見的工具,一些開源庫中也都有自己針對特定場景的實現(如 Redux、koa-compose)。而要實現一個通用的 compose 函數其實很簡單,借助數組的 reduce 方法就好

    const compose = (funcs) => {
      if (funcs.length === 0) {
        return (arg) => arg;
      }
      if (funcs.length === 1) {
        return funcs[0];
      }
      funcs.reduce(
        (pre, cur) =>
          (...args) =>
            pre(cur(...args))
      );
    };
    const fn1 = (x) => x * 2;
    const fn2 = (x) => x + 2;
    const fn3 = (x) => x * 3;
    const compute = compose([fn1, fn2, fn3]);
    // compute = (...args) => fn1(fn2(fn3(...args)))
    console.log(compute(1)); // 10

    pipe 函數與 compose 的區別則是其執行順序相反,正如其字面含義,就像 Linux 中的管道操作符,前一個函數的結果流向下一個函數的入參,所以把 reduce 方法改為 reduceRight 即可:

    const pipe = (funcs) => {
      if (funcs.length === 0) {
        return (arg) => arg;
      }
      if (funcs.length === 1) {
        return funcs[0];
      }
      funcs.reduceRight(
        (pre, cur) =>
          (...args) =>
            pre(cur(...args))
      );
    };
    const compute = pipe([fn1, fn2, fn3]);
    // compute = (...args) => fn3(fn2(fn1(...args)))
    console.log(compute(1)); // 12

    函數式在常見庫中的應用

    React

    在最新的 React 文檔中,函數式組件 + hook 寫法已經成為官方的首推風格。而這正是基于函數式編程的理念。React 的核心特征是“數據驅動視圖”,即UI = render(data)

    UI 的更新是一定需要副作用的,那么如何保證組件函數的“純”呢?答案是將副作用在組件之外進行管理,所有的副作用都交由 hooks,組件可以使用 state,但并不擁有 state

    Hooks 相比類組件的優點:

    • 關注點分離。在類組件中,邏輯代碼放在生命周期中,代碼是按照生命周期組織的。而在 hooks 寫法中,代碼按業務邏輯組織,更加清晰

    • 寫法更簡單。省去了類組件寫法中基于繼承的各種復雜設計模式

    Immutable.js

    Immutable是用于達成函數式編程三要素中的“不可變值”。我的初次接觸是在 Redux 中使用到,Redux 要求 reducer 中不能修改 state 而是應該返回新的 state,但這僅是一種“規范上的約定”,而不是“代碼層面的限制”,而 Immutable 正是用于提供 JS 原生不存在的不可修改的數據結構

    Immutable 提供了一系列自定義數據結構,并提供相應的更新 API,而這些 API 將通過返回新值的方式執行更新。

    let map1 = Immutable.Map({});
    map1 = map1.set("name", "youky");
    console.log(map1);

    Immutable 內部的存儲參考 字典樹(Trie) 實現,在每次修改時,不變的屬性將用索引指向原來的值,只對改變的值賦值新的索引。這樣更新的效率會比整體拷貝高很多。

    Redux

    Redux 中體現函數式編程模式的也有很多地方:

    • reducer 要是純函數(如果需要副作用,則使用 redux-saga 等中間件)

    • reducer 中不直接修改 state,而是返回新的 state

    • 中間件的高階函數與柯里化

    • 提供了一個 compose 函數,這是函數式編程中非常基本的工具函數

    Redux 源碼中的 compose 函數實現如下:

    export default function compose(): <R>(a: R) => R;
    export default function compose<F extends Function>(f: F): F;
    /* two functions */
    export default function compose<A, T extends any[], R>(
      f1: (a: A) => R,
      f2: Func<T, A>
    ): Func<T, R>;
    /* three functions */
    export default function compose<A, B, T extends any[], R>(
      f1: (b: B) => R,
      f2: (a: A) => B,
      f3: Func<T, A>
    ): Func<T, R>;
    /* four functions */
    export default function compose<A, B, C, T extends any[], R>(
      f1: (c: C) => R,
      f2: (b: B) => C,
      f3: (a: A) => B,
      f4: Func<T, A>
    ): Func<T, R>;
    /* rest */
    export default function compose<R>(
      f1: (a: any) => R,
      ...funcs: Function[]
    ): (...args: any[]) => R;
    export default function compose<R>(...funcs: Function[]): (...args: any[]) => R;
    export default function compose(...funcs: Function[]) {
      if (funcs.length === 0) {
        // infer the argument type so it is usable in inference down the line
        return <T>(arg: T) => arg;
      }
      if (funcs.length === 1) {
        return funcs[0];
      }
      return funcs.reduce(
        (a, b) =>
          (...args: any) =>
            a(b(...args))
      );
    }

    首先是用函數重載來進行類型聲明。

    在實現其實非常簡單:

    • 傳入數組為空,返回一個自定義函數,這個函數返回接收到的參數

    • 如果傳入數組長度為 1,返回唯一的一個元素

    • 使用 reduce 方法組裝數組元素,返回一個包含元素嵌套執行的新函數

    Koa

    在 Koa 的洋蔥模型中,通過 app.use 添加中間件,會將中間件函數存儲于this.middleware

    use (fn) {
        if (typeof fn !== 'function') throw new TypeError('middleware must be a function!')
        debug('use %s', fn._name || fn.name || '-')
        this.middleware.push(fn)
        return this
    }

    通過 koa-compose 模塊將所有的中間件組合為一個函數 fn,在每次處理請求時調用

    // callback 就是 app.listen 時綁定的處理函數
    callback () {
        const fn = this.compose(this.middleware)
        if (!this.listenerCount('error')) this.on('error', this.onerror)
        const handleRequest = (req, res) => {
          const ctx = this.createContext(req, res)
          return this.handleRequest(ctx, fn)
        }
        return handleRequest
    }

    這里的 compose 決定了多個中間件之間的調用順序,用戶可以通過 option 傳入自定義的 compose 函數,或默認使用 koa-compose 模塊。其源碼如下:

    function compose(middleware) {
      if (!Array.isArray(middleware))
        throw new TypeError("Middleware stack must be an array!");
      for (const fn of middleware) {
        if (typeof fn !== "function")
          throw new TypeError("Middleware must be composed of functions!");
      }
      /**
       * @param {Object} context
       * @return {Promise}
       * @api public
       */
      return function (context, next) {
        // last called middleware #
        let index = -1;
        return dispatch(0);
        function dispatch(i) {
          if (i <= index)
            return Promise.reject(new Error("next() called multiple times"));
          index = i;
          let fn = middleware[i];
          if (i === middleware.length) fn = next;
          if (!fn) return Promise.resolve();
          try {
            return Promise.resolve(fn(context, dispatch.bind(null, i + 1)));
          } catch (err) {
            return Promise.reject(err);
          }
        }
      };
    }

    同樣是先對參數進行判斷。與 redux 中的 compose 不同的是,koa 中的中間件是異步的,需要手動調用 next 方法將執行權交給下一個中間件。通過代碼可知,中間件中接收的 next 參數實際就是 dispatch.bind(null, i + 1))也就是 dispatch 方法,以達到遞歸執行的目的。

    這里使用 bind 實際上就是創建了一個偏函數。根據 bind 的定義,在 this 之后傳入的若干個參數會在返回函數調用時插入參數列表的最前面。也就是說

    const next = dispatch.bind(null, i + 1))
    next() // 等價于dispatch(i+1)

    附:函數式編程與數學原理

    函數并不是計算機領域的專有名詞。實際上,函數一詞最早由萊布尼茲在 1694 年開始使用。

    函數式編程的思想背后,其實蘊含了范疇論、群論等數學原理的思想。

    關于“Immutable.js到Redux函數式編程源碼分析”這篇文章的內容就介紹到這里,感謝各位的閱讀!相信大家對“Immutable.js到Redux函數式編程源碼分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。

    向AI問一下細節

    免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

    AI

    东源县| 绥芬河市| 大同县| 砀山县| 山东省| 鞍山市| 肇州县| 南部县| 霍林郭勒市| 茶陵县| 平遥县| 金溪县| 德令哈市| 淮南市| 竹山县| 沙湾县| 武鸣县| 嘉黎县| 子长县| 镇沅| 资讯| 四平市| 朝阳县| 繁峙县| 长宁区| 吴桥县| 晋中市| 故城县| 库尔勒市| 阿合奇县| 乐山市| 海林市| 石嘴山市| 平原县| 阜康市| 浠水县| 中西区| 绥宁县| 阿克苏市| 黄浦区| 泸定县|