您好,登錄后才能下訂單哦!
這篇文章主要介紹了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 是一種多范式語言,而從前端的發展歷史來看,各時段的主流框架,也正對應了三種編程范式:
JQuery:命令式編程
React 類組件:面向對象
React Hooks、 Vue3:函數式編程
利于更好的代碼組織。因為純函數不依賴于上下文所以天然具有高內聚低耦合的特點
利于邏輯復用。純函數的執行是與上下文無關的,因此可以更好的在不同場景中復用
便于單元測試。純函數對于相同輸入一定得到相同輸出的特點,便于自動化測試
相比于命令式編程,往往會包裝更多的方法,產生更多的上下文切換帶來的開銷。
更多的使用遞歸,導致更高的內存開銷。
為了實現不可變數據,會產生更多的對象,對垃圾回收的壓力更大。
偏函數的定義簡單來說就是,將函數轉換為參數更少的函數,也就是為其預設參數。
從 fn(arg1, arg2) 到 fn(arg1)
柯里化函數在偏函數的基礎上,不僅減少了函數入參個數,還改變了函數執行次數。其含義就是將一個接收 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 同樣是很常見的工具,一些開源庫中也都有自己針對特定場景的實現(如 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 文檔中,函數式組件 + hook 寫法已經成為官方的首推風格。而這正是基于函數式編程的理念。React 的核心特征是“數據驅動視圖”,即UI = render(data)
。
UI 的更新是一定需要副作用的,那么如何保證組件函數的“純”呢?答案是將副作用在組件之外進行管理,所有的副作用都交由 hooks,組件可以使用 state,但并不擁有 state。
Hooks 相比類組件的優點:
關注點分離。在類組件中,邏輯代碼放在生命周期中,代碼是按照生命周期組織的。而在 hooks 寫法中,代碼按業務邏輯組織,更加清晰
寫法更簡單。省去了類組件寫法中基于繼承的各種復雜設計模式
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 中體現函數式編程模式的也有很多地方:
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 的洋蔥模型中,通過 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函數式編程源碼分析”知識都有一定的了解,大家如果還想學習更多知識,歡迎關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。