亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

從Immutable.js到Redux函數(shù)式編程

 更新時間:2023年04月03日 14:43:00   作者:Youky  
這篇文章主要為大家介紹了從Immutable.js到Redux函數(shù)式編程示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪

基本概念

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

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

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

  • 函數(shù)是一等公民
  • 擁抱純函數(shù),拒絕副作用
  • 使用不可變值

函數(shù)式編程要素

函數(shù)是一等公民

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

  • 可以被當作參數(shù)傳遞給其他函數(shù)
  • 可以作為另一個函數(shù)的返回值
  • 可以被賦值給一個變量

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

純函數(shù)

有且僅有顯示數(shù)據(jù)流:

  • 輸入:參數(shù)
  • 輸出:返回值

一個函數(shù)要是純函數(shù),要符合以下幾點:

函數(shù)內部不能有副作用

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

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

副作用

參考純函數(shù)“僅有顯示數(shù)據(jù)流”的定義,副作用的定義即擁有“隱式數(shù)據(jù)流”?;蛘哒f:

  • 會對函數(shù)作用域之外的執(zhí)行上下文、宿主環(huán)境產(chǎn)生影響,如修改全局變量
  • 依賴了隱式輸入,如使用全局變量
  • 進行了與外界的隱式數(shù)據(jù)交換,如網(wǎng)絡請求

不可變值

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

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

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

如果真的需要設計對數(shù)據(jù)的修改,則應該:

  • 拷貝原始數(shù)據(jù)
  • 修改拷貝結果,返回新的數(shù)據(jù)
const reverse = (arr) => {
  const temp = JSON.parse(JSON.stringify(arr));
  return temp.reverse();
};
arr = reverse(arr);

拷貝帶來的問題

通過拷貝實現(xiàn)對外部數(shù)據(jù)的只讀直觀且簡單,代價則是性能。

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

解決拷貝的性能問題: 持久化數(shù)據(jù)結構

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

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

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

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

JS 中三種編程范式

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

  • JQuery:命令式編程
  • React 類組件:面向對象
  • React Hooks、 Vue3:函數(shù)式編程

函數(shù)式編程的優(yōu)缺點

優(yōu)點

  • 利于更好的代碼組織。因為純函數(shù)不依賴于上下文所以天然具有高內聚低耦合的特點
  • 利于邏輯復用。純函數(shù)的執(zhí)行是與上下文無關的,因此可以更好的在不同場景中復用
  • 便于單元測試。純函數(shù)對于相同輸入一定得到相同輸出的特點,便于自動化測試

缺點

  • 相比于命令式編程,往往會包裝更多的方法,產(chǎn)生更多的上下文切換帶來的開銷。
  • 更多的使用遞歸,導致更高的內存開銷。
  • 為了實現(xiàn)不可變數(shù)據(jù),會產(chǎn)生更多的對象,對垃圾回收的壓力更大。

偏函數(shù)

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

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

柯里化(curry)函數(shù)

柯里化函數(shù)在偏函數(shù)的基礎上,不僅減少了函數(shù)入?yún)€數(shù),還改變了函數(shù)執(zhí)行次數(shù)。其含義就是將一個接收 N 個入?yún)⒌暮瘮?shù),改寫為接受一個入?yún)?,并返回接受剩?N-1 個參數(shù)的函數(shù)。也就是:

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

實現(xiàn)一個柯里化函數(shù)也是面試高頻內容,其實如果規(guī)定了函數(shù)入?yún)€數(shù),那么是很容易實現(xiàn)的。例如對于入?yún)€數(shù)為 3 的函數(shù),實現(xiàn)如下

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

那么實現(xiàn)通用的 curry 函數(shù)的關鍵就在于:

  • 自動判斷函數(shù)入?yún)?/li>
  • 自我遞歸調用
const curry = (fn) => {
  const argLen = fn.length; // 原函數(shù)的入?yún)€數(shù)
  const recursion = (args) =>
    args.length >= argLen
      ? fn(...args)
      : (newArg) => recursion([...args, newArg]);
  return recursion([]);
};

compose & pipe

compose 和 pipe 同樣是很常見的工具,一些開源庫中也都有自己針對特定場景的實現(xiàn)(如 Redux、koa-compose)。而要實現(xiàn)一個通用的 compose 函數(shù)其實很簡單,借助數(shù)組的 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 函數(shù)與 compose 的區(qū)別則是其執(zhí)行順序相反,正如其字面含義,就像 Linux 中的管道操作符,前一個函數(shù)的結果流向下一個函數(shù)的入?yún)?,所以?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

函數(shù)式在常見庫中的應用

React

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

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

Hooks 相比類組件的優(yōu)點:

  • 關注點分離。在類組件中,邏輯代碼放在生命周期中,代碼是按照生命周期組織的。而在 hooks 寫法中,代碼按業(yè)務邏輯組織,更加清晰
  • 寫法更簡單。省去了類組件寫法中基于繼承的各種復雜設計模式

Immutable.js

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

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

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

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

Redux

Redux 中體現(xiàn)函數(shù)式編程模式的也有很多地方:

  • reducer 要是純函數(shù)(如果需要副作用,則使用 redux-saga 等中間件)
  • reducer 中不直接修改 state,而是返回新的 state
  • 中間件的高階函數(shù)與柯里化
  • 提供了一個 compose 函數(shù),這是函數(shù)式編程中非?;镜墓ぞ吆瘮?shù)

Redux 源碼中的 compose 函數(shù)實現(xiàn)如下:

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

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

在實現(xiàn)其實非常簡單:

  • 傳入數(shù)組為空,返回一個自定義函數(shù),這個函數(shù)返回接收到的參數(shù)
  • 如果傳入數(shù)組長度為 1,返回唯一的一個元素
  • 使用 reduce 方法組裝數(shù)組元素,返回一個包含元素嵌套執(zhí)行的新函數(shù)

Koa

在 Koa 的洋蔥模型中,通過 app.use 添加中間件,會將中間件函數(shù)存儲于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 模塊將所有的中間件組合為一個函數(shù) fn,在每次處理請求時調用

// callback 就是 app.listen 時綁定的處理函數(shù)
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 函數(shù),或默認使用 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);
      }
    }
  };
}

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

這里使用 bind 實際上就是創(chuàng)建了一個偏函數(shù)。根據(jù) bind 的定義,在 this 之后傳入的若干個參數(shù)會在返回函數(shù)調用時插入?yún)?shù)列表的最前面。也就是說

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

附:函數(shù)式編程與數(shù)學原理

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

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

以上就是從Immutable.js到Redux函數(shù)式編程的詳細內容,更多關于Immutable.js Redux函數(shù)式編程的資料請關注腳本之家其它相關文章!

相關文章

最新評論