欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

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

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

基本概念

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

以上是維基百科對(duì)于函數(shù)式編程的定義,用簡(jiǎn)單的話總結(jié)就是“強(qiáng)調(diào)以函數(shù)使用為主的軟件開發(fā)風(fēng)格”。

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

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

函數(shù)式編程要素

函數(shù)是一等公民

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

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

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

純函數(shù)

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

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

一個(gè)函數(shù)要是純函數(shù),要符合以下幾點(diǎn):

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

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

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

副作用

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

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

不可變值

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

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

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

如果真的需要設(shè)計(jì)對(duì)數(shù)據(jù)的修改,則應(yīng)該:

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

拷貝帶來的問題

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

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

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

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

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

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

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

JS 中三種編程范式

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

  • JQuery:命令式編程
  • React 類組件:面向?qū)ο?/li>
  • React Hooks、 Vue3:函數(shù)式編程

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

優(yōu)點(diǎn)

  • 利于更好的代碼組織。因?yàn)榧兒瘮?shù)不依賴于上下文所以天然具有高內(nèi)聚低耦合的特點(diǎn)
  • 利于邏輯復(fù)用。純函數(shù)的執(zhí)行是與上下文無關(guān)的,因此可以更好的在不同場(chǎng)景中復(fù)用
  • 便于單元測(cè)試。純函數(shù)對(duì)于相同輸入一定得到相同輸出的特點(diǎn),便于自動(dòng)化測(cè)試

缺點(diǎn)

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

偏函數(shù)

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

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

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

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

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

實(shí)現(xiàn)一個(gè)柯里化函數(shù)也是面試高頻內(nèi)容,其實(shí)如果規(guī)定了函數(shù)入?yún)€(gè)數(shù),那么是很容易實(shí)現(xiàn)的。例如對(duì)于入?yún)€(gè)數(shù)為 3 的函數(shù),實(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

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

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

compose & pipe

compose 和 pipe 同樣是很常見的工具,一些開源庫中也都有自己針對(duì)特定場(chǎng)景的實(shí)現(xiàn)(如 Redux、koa-compose)。而要實(shí)現(xiàn)一個(gè)通用的 compose 函數(shù)其實(shí)很簡(jiǎn)單,借助數(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 中的管道操作符,前一個(gè)函數(shù)的結(jié)果流向下一個(gè)函數(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ù)式在常見庫中的應(yīng)用

React

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

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

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

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

Immutable.js

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

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

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

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

Redux

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

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

Redux 源碼中的 compose 函數(shù)實(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ù)重載來進(jìn)行類型聲明。

在實(shí)現(xiàn)其實(shí)非常簡(jiǎn)單:

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

Koa

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

// callback 就是 app.listen 時(shí)綁定的處理函數(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 決定了多個(gè)中間件之間的調(diào)用順序,用戶可以通過 option 傳入自定義的 compose 函數(shù),或默認(rèn)使用 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);
      }
    }
  };
}

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

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

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

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

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

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

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

相關(guān)文章

最新評(píng)論